Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-26481] More clearly report problem passing closures to binary methods #15

Merged
merged 7 commits into from
Jun 10, 2016
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
package org.jenkinsci.plugins.workflow.cps;

import com.cloudbees.groovy.cps.impl.CpsClosure;
import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader;
import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.EnumeratingWhitelist;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist;

/**
* Allow any calls into the scripted compiled in the sandbox.
*
* IOW, allow the user to call his own methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not expect other plugins to have classes in the org.jenkinsci.plugins.workflow.cps package, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

*
* @author Kohsuke Kawaguchi
*/
class GroovyClassLoaderWhitelist extends Whitelist {

private final ClassLoader scriptLoader;

public GroovyClassLoaderWhitelist(GroovyClassLoader scriptLoader) {
/**
* {@link ProxyWhitelist} has an optimization which bypasses {@code permits*} calls
* when it detects {@link EnumeratingWhitelist} delegates,
* so we must do delegation manually.
* For the same reason, doing this check inside {@link CpsWhitelist} is tricky.
*/
private final Whitelist delegate;

GroovyClassLoaderWhitelist(GroovyClassLoader scriptLoader, Whitelist delegate) {
this.scriptLoader = scriptLoader;
this.delegate = delegate;
}

private boolean permits(Class<?> declaringClass) {
Expand All @@ -30,30 +45,48 @@ private boolean permits(Class<?> declaringClass) {
}

@Override public boolean permitsMethod(Method method, Object receiver, Object[] args) {
return permits(method.getDeclaringClass());
return checkJenkins26481(args, method, method.getParameterTypes()) || delegate.permitsMethod(method, receiver, args);
}

@Override public boolean permitsConstructor(Constructor<?> constructor, Object[] args) {
return permits(constructor.getDeclaringClass());
return checkJenkins26481(args, constructor, constructor.getParameterTypes()) || delegate.permitsConstructor(constructor, args);
}

@Override public boolean permitsStaticMethod(Method method, Object[] args) {
return permits(method.getDeclaringClass());
return checkJenkins26481(args, method, method.getParameterTypes()) || delegate.permitsStaticMethod(method, args);
}

@Override public boolean permitsFieldGet(Field field, Object receiver) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsFieldGet(field, receiver);
}

@Override public boolean permitsFieldSet(Field field, Object receiver, Object value) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsFieldSet(field, receiver, value);
}

@Override public boolean permitsStaticFieldGet(Field field) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldGet(field);
}

@Override public boolean permitsStaticFieldSet(Field field, Object value) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldSet(field, value);
}

/**
* Blocks accesses to some {@link DefaultGroovyMethods}, or any other binary method taking a closure, until JENKINS-26481 is fixed.
* Only improves diagnostics for scripts using the sandbox.
* Note that we do not simply return false for these cases, as that would throw {@link RejectedAccessException} without a meaningful explanation.
*/
private boolean checkJenkins26481(Object[] args, /* TODO Java 8: just take Executable */ Member method, Class<?>[] parameterTypes) throws UnsupportedOperationException {
if (permits(method.getDeclaringClass())) { // fine for source-defined methods to take closures
return true;
}
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof CpsClosure && parameterTypes.length > i && parameterTypes[i] == Closure.class) {
throw new UnsupportedOperationException("Calling " + method + " on a CPS-transformed closure is not yet supported (JENKINS-26481); encapsulate in a @NonCPS method, or use Java-style loops");
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.cloudbees.groovy.cps.Outcome;
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;

Expand Down Expand Up @@ -37,7 +36,7 @@ public Outcome call() {
}
return outcome;
}
}, new ProxyWhitelist(new GroovyClassLoaderWhitelist(thread.group.getExecution().getShell().getClassLoader()),CpsWhitelist.get()));
}, new GroovyClassLoaderWhitelist(thread.group.getExecution().getShell().getClassLoader(), CpsWhitelist.get()));
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jenkinsci.plugins.workflow;

import groovy.lang.Closure;
import hudson.model.Result;
import hudson.slaves.DumbSlave;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
Expand Down Expand Up @@ -234,6 +235,43 @@ public void stop(Throwable cause) throws Exception {
});
}

/**
* Verifies that we are not throwing {@link UnsupportedOperationException} too aggressively.
* In particular:
* <ul>
* <li>on non-CPS-transformed {@link Closure}s
* <li>on closures passed to methods defined in Pipeline script
* <li>on closures passed to methods which did not declare {@link Closure} as a parameter type and so presumably are not going to try to call them
* </ul>
*/
@Issue("JENKINS-26481")
@Test public void eachClosureNonCps() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verifies that we are not throwing the UnsupportedOperationException too aggressively:

  • on non-CPS-transformed Closures
  • on closures passed to methods defined in Pipeline script
  • on closures passed to methods which did not declare Closure as a parameter type and so presumably are not going to try to call them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to put the explanation to Javadoc directly

story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.Collection java.lang.Object");
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"@NonCPS def fine() {\n" +
" def text = ''\n" +
" ['a', 'b', 'c'].each {it -> text += it}\n" +
" text\n" +
"}\n" +
"def takesMyOwnClosure(body) {\n" +
" node {\n" +
" def list = []\n" +
" list += body\n" +
" echo list[0]()\n" +
" }\n" +
"}\n" +
"takesMyOwnClosure {\n" +
" fine()\n" +
"}\n", true));
b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("abc", b);
}
});
}

@Ignore("TODO JENKINS-31314: calls writeFile just once, echoes null (i.e., return value of writeFile), then succeeds")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still fails with the same behavior.

@Test public void nonCpsContinuable() {
story.addStep(new Statement() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.RestartableJenkinsRule;

Expand All @@ -19,6 +21,8 @@
* @author Kohsuke Kawaguchi
*/
public class RestartingLoadStepTest {

@ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
@Rule public RestartableJenkinsRule story = new RestartableJenkinsRule();

@Inject
Expand All @@ -34,8 +38,8 @@ public void persistenceOfLoadedScripts() throws Exception {
WorkflowJob p = jenkins.createProject(WorkflowJob.class, "p");
jenkins.getWorkspaceFor(p).child("test.groovy").write(
"def answer(i) { return i*2; }\n" +
"def foo() {\n" +
" def i=21;\n" +
"def foo(body) {\n" +
" def i = body()\n" +
" semaphore 'watchA'\n" +
" return answer(i);\n" +
"}\n" +
Expand All @@ -44,7 +48,7 @@ public void persistenceOfLoadedScripts() throws Exception {
"node {\n" +
" println 'started'\n" +
" def o = load 'test.groovy'\n" +
" println 'o=' + o.foo();\n" +
" println 'o=' + o.foo({21})\n" +
"}"));

// get the build going
Expand Down