Skip to content

Commit 0a154af

Browse files
committed
script performant multi-run support
1 parent 7ff8aed commit 0a154af

File tree

3 files changed

+133
-46
lines changed

3 files changed

+133
-46
lines changed

src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovySandbox.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,14 @@ public void run() {
126126
/**
127127
* Runs a script in the sandbox.
128128
* You must have used {@link #createSecureCompilerConfiguration} to prepare the Groovy shell.
129+
* Before running a sandboxed script, always {@link org.kohsuke.groovy.sandbox.GroovyInterceptor#register register} and after that make sure to {@link org.kohsuke.groovy.sandbox.GroovyInterceptor#unregister unregister}
129130
* @param script a script ready to {@link Script#run}, created for example by {@link GroovyShell#parse(String)}
130131
* @param whitelist the whitelist to use, such as {@link Whitelist#all()}
131132
* @return the value produced by the script, if any
132133
* @throws RejectedAccessException in case an attempted call was not whitelisted
133134
*/
134135
public static Object run(@Nonnull Script script, @Nonnull final Whitelist whitelist) throws RejectedAccessException {
135-
Whitelist wrapperWhitelist = new ProxyWhitelist(
136-
new ClassLoaderWhitelist(script.getClass().getClassLoader()),
137-
whitelist);
138-
GroovyInterceptor sandbox = new SandboxInterceptor(wrapperWhitelist);
136+
GroovyInterceptor sandbox = createSandbox(script, whitelist);
139137
sandbox.register();
140138
try {
141139
return script.run();
@@ -144,6 +142,20 @@ public static Object run(@Nonnull Script script, @Nonnull final Whitelist whitel
144142
}
145143
}
146144

145+
/**
146+
* Prepares a sandbox for a script.
147+
* You must have used {@link #createSecureCompilerConfiguration} to prepare the Groovy shell.
148+
* @param script a script ready to {@link Script#run}, created for example by {@link GroovyShell#parse(String)}
149+
* @param whitelist the whitelist to use, such as {@link Whitelist#all()}
150+
* @return the sandbox for running this script
151+
*/
152+
public static GroovyInterceptor createSandbox(@Nonnull Script script, @Nonnull final Whitelist whitelist) {
153+
Whitelist wrapperWhitelist = new ProxyWhitelist(
154+
new ClassLoaderWhitelist(script.getClass().getClassLoader()),
155+
whitelist);
156+
return new SandboxInterceptor(wrapperWhitelist);
157+
}
158+
147159
private GroovySandbox() {}
148160

149161
}

src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import groovy.lang.Binding;
2929
import groovy.lang.GroovyClassLoader;
3030
import groovy.lang.GroovyShell;
31+
import groovy.lang.Script;
3132
import hudson.Extension;
3233
import hudson.PluginManager;
3334
import hudson.model.AbstractDescribableImpl;
@@ -36,6 +37,8 @@
3637
import hudson.util.FormValidation;
3738

3839
import java.beans.Introspector;
40+
import java.io.IOException;
41+
import java.io.Closeable;
3942
import java.lang.ref.Reference;
4043
import java.lang.ref.WeakReference;
4144
import java.lang.reflect.Field;
@@ -68,6 +71,7 @@
6871
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedClasspathException;
6972
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
7073
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
74+
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
7175
import org.kohsuke.stapler.DataBoundConstructor;
7276
import org.kohsuke.stapler.QueryParameter;
7377
import org.kohsuke.stapler.Stapler;
@@ -277,6 +281,17 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro
277281
}
278282
}
279283

284+
285+
/**
286+
* Prepares the Groovy script to be executed.
287+
* @param loader see parameter explanation under {@link #evaluate(ClassLoader, Binding)}
288+
* @param binding see parameter explanation under {@link #evaluate(ClassLoader, Binding)}
289+
* @return the prepared script
290+
*/
291+
public PreparedScript prepare(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException {
292+
return new PreparedScript(loader, binding);
293+
}
294+
280295
/**
281296
* Runs the Groovy script, using the sandbox if so configured.
282297
* @param loader a class loader for constructing the shell, such as {@link PluginManager#uberClassLoader} (will be augmented by {@link #getClasspath} if nonempty)
@@ -287,63 +302,96 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro
287302
* @throws UnapprovedUsageException in case of a non-sandbox issue
288303
* @throws UnapprovedClasspathException in case some unapproved classpath entries were requested
289304
*/
290-
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.")
291305
public Object evaluate(ClassLoader loader, Binding binding) throws Exception {
292-
if (!calledConfiguring) {
293-
throw new IllegalStateException("you need to call configuring or a related method before using GroovyScript");
294-
}
295-
URLClassLoader urlcl = null;
296-
ClassLoader memoryProtectedLoader = null;
297-
List<ClasspathEntry> cp = getClasspath();
298-
if (!cp.isEmpty()) {
299-
List<URL> urlList = new ArrayList<URL>(cp.size());
300-
301-
for (ClasspathEntry entry : cp) {
302-
ScriptApproval.get().using(entry);
303-
urlList.add(entry.getURL());
304-
}
305-
306-
loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader);
306+
try (PreparedScript scriptInstance = prepare(loader, binding)) {
307+
return scriptInstance.run();
307308
}
308-
boolean canDoCleanup = false;
309+
}
309310

310-
try {
311-
loader = GroovySandbox.createSecureClassLoader(loader);
311+
/**
312+
* Allows access to execute a script multiple times without preparation and clean-up overhead.
313+
* While the intricate logic to do this continues to be hidden from users.
314+
*/
315+
public final class PreparedScript implements Closeable {
316+
317+
private final GroovyShell sh;
318+
private final Script preparedScript;
319+
private ClassLoader memoryProtectedLoader = null;
320+
private GroovyInterceptor scriptSandbox = null;
321+
private URLClassLoader urlcl = null;
322+
private boolean canDoCleanup = false;
323+
324+
/**
325+
* @see @see SecureGroovyScript#evaluate()
326+
*/
327+
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.")
328+
private PreparedScript(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException {
329+
List<ClasspathEntry> cp = getClasspath();
330+
if (!cp.isEmpty()) {
331+
List<URL> urlList = new ArrayList<URL>(cp.size());
332+
333+
for (ClasspathEntry entry : cp) {
334+
ScriptApproval.get().using(entry);
335+
urlList.add(entry.getURL());
336+
}
312337

313-
Field loaderF = null;
314-
try {
315-
loaderF = GroovyShell.class.getDeclaredField("loader");
316-
loaderF.setAccessible(true);
317-
canDoCleanup = true;
318-
} catch (NoSuchFieldException nsme) {
319-
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work");
338+
loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader);
320339
}
321340

322-
GroovyShell sh;
323-
if (sandbox) {
324-
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration();
325-
sh = new GroovyShell(loader, binding, cc);
341+
try {
342+
loader = GroovySandbox.createSecureClassLoader(loader);
343+
Field loaderF = null;
344+
try {
345+
loaderF = GroovyShell.class.getDeclaredField("loader");
346+
loaderF.setAccessible(true);
347+
canDoCleanup = true;
348+
} catch (NoSuchFieldException nsme) {
349+
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work");
350+
}
326351

327-
if (canDoCleanup) {
328-
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc);
329-
loaderF.set(sh, memoryProtectedLoader);
352+
if (sandbox) {
353+
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration();
354+
sh = new GroovyShell(loader, binding, cc);
355+
356+
if (canDoCleanup) {
357+
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc);
358+
loaderF.set(sh, memoryProtectedLoader);
359+
}
360+
361+
preparedScript = sh.parse(script);
362+
scriptSandbox = GroovySandbox.createSandbox(preparedScript, Whitelist.all());
363+
} else {
364+
sh = new GroovyShell(loader, binding);
365+
if (canDoCleanup) {
366+
memoryProtectedLoader = new CleanGroovyClassLoader(loader);
367+
loaderF.set(sh, memoryProtectedLoader);
368+
}
369+
370+
preparedScript = sh.parse(ScriptApproval.get().using(script, GroovyLanguage.get()));
330371
}
372+
} catch (Exception e) {
373+
close();
374+
throw e;
375+
}
376+
}
331377

378+
public Object run() throws Exception {
379+
if (sandbox) {
380+
scriptSandbox.register();
332381
try {
333-
return GroovySandbox.run(sh.parse(script), Whitelist.all());
382+
return preparedScript.run();
334383
} catch (RejectedAccessException x) {
335384
throw ScriptApproval.get().accessRejected(x, ApprovalContext.create());
385+
} finally {
386+
scriptSandbox.unregister();
336387
}
337388
} else {
338-
sh = new GroovyShell(loader, binding);
339-
if (canDoCleanup) {
340-
memoryProtectedLoader = new CleanGroovyClassLoader(loader);
341-
loaderF.set(sh, memoryProtectedLoader);
342-
}
343-
return sh.evaluate(ScriptApproval.get().using(script, GroovyLanguage.get()));
389+
return preparedScript.run();
344390
}
391+
}
345392

346-
} finally {
393+
@Override
394+
public void close() throws IOException {
347395
try {
348396
if (canDoCleanup) {
349397
cleanUpLoader(memoryProtectedLoader, new HashSet<ClassLoader>(), new HashSet<Class<?>>());

src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,5 +802,32 @@ public void testSandboxClassResolution() throws Exception {
802802
assertTrue(e.getMessage().contains("staticMethod java.lang.System gc"));
803803
}
804804
}
805-
805+
806+
@Test public void testSandboxUsesSelectedBinding() throws Exception {
807+
SecureGroovyScript sgs = new SecureGroovyScript("return a++", true, null);
808+
Binding b = new Binding();
809+
b.setVariable("a", 5);
810+
try(SecureGroovyScript.PreparedScript script = sgs.configuringWithKeyItem().prepare(Jenkins.getInstance().getPluginManager().uberClassLoader, b)) {
811+
Object res = script.run();
812+
assertTrue((int) res == 5);
813+
assertTrue((int) b.getVariable("a") == 6);
814+
res = script.run();
815+
assertTrue((int) res == 6);
816+
assertTrue((int) b.getVariable("a") == 7);
817+
}
818+
}
819+
820+
@Test public void testNonSandboxUsesSelectedBinding() throws Exception {
821+
SecureGroovyScript sgs = new SecureGroovyScript("return a++", false, null);
822+
Binding b = new Binding();
823+
b.setVariable("a", 5);
824+
try (SecureGroovyScript.PreparedScript script = sgs.configuringWithKeyItem().prepare(Jenkins.getInstance().getPluginManager().uberClassLoader, b)) {
825+
Object res = script.run();
826+
assertTrue((int) res == 5);
827+
assertTrue((int) b.getVariable("a") == 6);
828+
res = script.run();
829+
assertTrue((int) res == 6);
830+
assertTrue((int) b.getVariable("a") == 7);
831+
}
832+
}
806833
}

0 commit comments

Comments
 (0)