Skip to content

Commit

Permalink
script performant multi-run support
Browse files Browse the repository at this point in the history
  • Loading branch information
akostadinov committed Dec 4, 2017
1 parent 7ff8aed commit 56ae4c8
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class GroovySandbox {
public static @Nonnull ClassLoader createSecureClassLoader(ClassLoader base) {
return new SandboxResolvingClassLoader(base);
}

/**
* Runs a block in the sandbox.
* You must have used {@link #createSecureCompilerConfiguration} to prepare the Groovy shell.
Expand Down Expand Up @@ -132,10 +132,7 @@ public void run() {
* @throws RejectedAccessException in case an attempted call was not whitelisted
*/
public static Object run(@Nonnull Script script, @Nonnull final Whitelist whitelist) throws RejectedAccessException {
Whitelist wrapperWhitelist = new ProxyWhitelist(
new ClassLoaderWhitelist(script.getClass().getClassLoader()),
whitelist);
GroovyInterceptor sandbox = new SandboxInterceptor(wrapperWhitelist);
GroovyInterceptor sandbox = createSandbox(script, whitelist);
sandbox.register();
try {
return script.run();
Expand All @@ -144,6 +141,20 @@ public static Object run(@Nonnull Script script, @Nonnull final Whitelist whitel
}
}

/**
* Prepares a sandbox for a script.
* You must have used {@link #createSecureCompilerConfiguration} to prepare the Groovy shell.
* @param script a script ready to {@link Script#run}, created for example by {@link GroovyShell#parse(String)}
* @param whitelist the whitelist to use, such as {@link Whitelist#all()}
* @return the sandbox for running this script
*/
public static GroovyInterceptor createSandbox(@Nonnull Script script, @Nonnull final Whitelist whitelist) {
Whitelist wrapperWhitelist = new ProxyWhitelist(
new ClassLoaderWhitelist(script.getClass().getClassLoader()),
whitelist);
return new SandboxInterceptor(wrapperWhitelist);
}

private GroovySandbox() {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import hudson.Extension;
import hudson.PluginManager;
import hudson.model.AbstractDescribableImpl;
Expand All @@ -36,6 +37,7 @@
import hudson.util.FormValidation;

import java.beans.Introspector;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
Expand Down Expand Up @@ -68,6 +70,7 @@
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedClasspathException;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
Expand All @@ -80,7 +83,7 @@
* Use {@code <f:property field="…"/>} to configure it from Jelly.
*/
public final class SecureGroovyScript extends AbstractDescribableImpl<SecureGroovyScript> {

private final @Nonnull String script;
private final boolean sandbox;
private final @CheckForNull List<ClasspathEntry> classpath;
Expand Down Expand Up @@ -277,6 +280,10 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro
}
}

public PreparedScript prepare(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException {
return new PreparedScript(loader, binding);
}

/**
* Runs the Groovy script, using the sandbox if so configured.
* @param loader a class loader for constructing the shell, such as {@link PluginManager#uberClassLoader} (will be augmented by {@link #getClasspath} if nonempty)
Expand All @@ -287,63 +294,99 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro
* @throws UnapprovedUsageException in case of a non-sandbox issue
* @throws UnapprovedClasspathException in case some unapproved classpath entries were requested
*/
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.")
public Object evaluate(ClassLoader loader, Binding binding) throws Exception {
if (!calledConfiguring) {
throw new IllegalStateException("you need to call configuring or a related method before using GroovyScript");
}
URLClassLoader urlcl = null;
ClassLoader memoryProtectedLoader = null;
List<ClasspathEntry> cp = getClasspath();
if (!cp.isEmpty()) {
List<URL> urlList = new ArrayList<URL>(cp.size());

for (ClasspathEntry entry : cp) {
ScriptApproval.get().using(entry);
urlList.add(entry.getURL());
}

loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader);
}
boolean canDoCleanup = false;
PreparedScript scriptInstance = prepare(loader, binding);

try {
loader = GroovySandbox.createSecureClassLoader(loader);
return scriptInstance.run();
} finally {
scriptInstance.cleanUp();
}
}

Field loaderF = null;
try {
loaderF = GroovyShell.class.getDeclaredField("loader");
loaderF.setAccessible(true);
canDoCleanup = true;
} catch (NoSuchFieldException nsme) {
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work");
/**
* Allows access to execue a script multiple times without preparation and clean-up overhead.
* While the intricate logic to do this continues to be hidden from users.
*/
public final class PreparedScript {

private final GroovyShell sh;
private final Script preparedScript;
private ClassLoader memoryProtectedLoader = null;
private GroovyInterceptor scriptSandbox = null;
private URLClassLoader urlcl = null;
private boolean canDoCleanup = false;

/**
* @see @see SecureGroovyScript#evaluate()
*/
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.")
private PreparedScript(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException {
List<ClasspathEntry> cp = getClasspath();
if (!cp.isEmpty()) {
List<URL> urlList = new ArrayList<URL>(cp.size());

for (ClasspathEntry entry : cp) {
ScriptApproval.get().using(entry);
urlList.add(entry.getURL());
}

loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader);
}

GroovyShell sh;
if (sandbox) {
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration();
sh = new GroovyShell(loader, binding, cc);
try {
loader = GroovySandbox.createSecureClassLoader(loader);
Field loaderF = null;
try {
loaderF = GroovyShell.class.getDeclaredField("loader");
loaderF.setAccessible(true);
canDoCleanup = true;
} catch (NoSuchFieldException nsme) {
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work");
}

if (canDoCleanup) {
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc);
loaderF.set(sh, memoryProtectedLoader);
if (sandbox) {
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration();
sh = new GroovyShell(loader, binding, cc);

if (canDoCleanup) {
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc);
loaderF.set(sh, memoryProtectedLoader);
}

preparedScript = sh.parse(script);
scriptSandbox = GroovySandbox.createSandbox(preparedScript, Whitelist.all());
} else {
sh = new GroovyShell(loader, binding);
if (canDoCleanup) {
memoryProtectedLoader = new CleanGroovyClassLoader(loader);
loaderF.set(sh, memoryProtectedLoader);
}

preparedScript = sh.parse(ScriptApproval.get().using(script, GroovyLanguage.get()));
}
} catch (Exception e) {
cleanUp();
throw e;
}
}

public Object run() throws Exception {
if (sandbox) {
scriptSandbox.register();
try {
return GroovySandbox.run(sh.parse(script), Whitelist.all());
return preparedScript.run();
} catch (RejectedAccessException x) {
throw ScriptApproval.get().accessRejected(x, ApprovalContext.create());
} finally {
scriptSandbox.unregister();
}
} else {
sh = new GroovyShell(loader, binding);
if (canDoCleanup) {
memoryProtectedLoader = new CleanGroovyClassLoader(loader);
loaderF.set(sh, memoryProtectedLoader);
}
return sh.evaluate(ScriptApproval.get().using(script, GroovyLanguage.get()));
return preparedScript.run();
}
}

} finally {
public void cleanUp() throws IOException {
try {
if (canDoCleanup) {
cleanUpLoader(memoryProtectedLoader, new HashSet<ClassLoader>(), new HashSet<Class<?>>());
Expand Down
Loading

0 comments on commit 56ae4c8

Please sign in to comment.