Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-42129

Closure delegate idiom fails attempting to call .DefaultGroovyMethods.invokeMethod(Object, String, Object)

      I have a simple Jenkinsfile that uses a (global) library method to wrap a closure. In the closure, I want to be able to optionally set some values that are picked up by the wrapper and acted on.

      Things work fine if I don't set the delegate on the closure. But when I do, I get the following build failure due to security violation when trying to run the 'node' step.

      [Pipeline] catchError
      [Pipeline] {
      [Pipeline] }
      org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:192)
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:95)
      	at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:149)
      	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:146)
      	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:123)
      	at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:16)
      	at WorkflowScript.run(WorkflowScript:6)
      	at emailBreaks.call(/home/jenkins/jobs/build-failures/builds/21/libs/myPipelineLib/vars/emailBreaks.groovy:9)
      	at ___cps.transform___(Native Method)
      	at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:57)
      	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.dispatchOrArg(FunctionCallBlock.java:109)
      	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.fixArg(FunctionCallBlock.java:82)
      	at sun.reflect.GeneratedMethodAccessor379.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      	at java.lang.reflect.Method.invoke(Method.java:498)
      	at com.cloudbees.groovy.cps.impl.ContinuationPtr$ContinuationImpl.receive(ContinuationPtr.java:72)
      	at com.cloudbees.groovy.cps.impl.ClosureBlock.eval(ClosureBlock.java:46)
      	at com.cloudbees.groovy.cps.Next.step(Next.java:74)
      	at com.cloudbees.groovy.cps.Continuable.run0(Continuable.java:154)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.access$001(SandboxContinuable.java:18)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable$1.call(SandboxContinuable.java:33)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable$1.call(SandboxContinuable.java:30)
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox.runInSandbox(GroovySandbox.java:108)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.run0(SandboxContinuable.java:30)
      	at org.jenkinsci.plugins.workflow.cps.CpsThread.runNextChunk(CpsThread.java:163)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:328)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$100(CpsThreadGroup.java:80)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:240)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:228)
      	at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:63)
      	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
      	at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:112)
      	at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
      	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
      	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
      	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
      	at java.lang.Thread.run(Thread.java:745)
      [Pipeline] // catchError
      [Pipeline] node
      Running on master in /home/jenkins/workspace/build-failures
      [Pipeline] {
      [Pipeline] emailextrecipients
      [Pipeline] step
      Sending e-mails to: user@example.com
      [Pipeline] }
      [Pipeline] // node
      [Pipeline] End of Pipeline
      Finished: FAILURE
      

          [JENKINS-42129] Closure delegate idiom fails attempting to call .DefaultGroovyMethods.invokeMethod(Object, String, Object)

          Richard Lee created issue -
          Richard Lee made changes -
          Issue Type Original: New Feature [ 2 ] New: Bug [ 1 ]

          Richard Lee added a comment -

          Note that it seems that the resolveStrategy is the key item. If set to Closure.DELEGATE_FIRST, you get the security violation. If left at the default (owner first), no security violation. Of course, the code doesn't work correctly in that case.

          Richard Lee added a comment - Note that it seems that the resolveStrategy is the key item. If set to Closure.DELEGATE_FIRST, you get the security violation. If left at the default (owner first), no security violation. Of course, the code doesn't work correctly in that case.

          Jesse Glick added a comment -

          No idea offhand, would need to spend time digging into what Groovy is trying to call and why the sandbox interceptor is misinterpreting that.

          In general I do not advise use of the closure-delegate idiom in Pipeline libraries. It is subtle and easy to get wrong. Better to have a plain old method taking positional or named (~ map) arguments, some of which may be of type Closure. Something like (untested):

          def call(Closure body, String mailingList = 'builds-list@example.com') {
              catchError {
                  body()
              }
          
              (currentBuild.result != 'ABORTED') && node('master') {
                  // send e-mail notifications for failed or unstable builds
                  def to = emailextrecipients([
                      [$class: 'CulpritsRecipientProvider'],
                      [$class: 'DevelopersRecipientProvider'],
                      [$class: 'RequesterRecipientProvider'],
                      [$class: 'UpstreamComitterRecipientProvider']
                  ]) + ' ' + mailingList
                  step([$class: 'Mailer',
                      notifyEveryUnstableBuild: true,
                      recipients: to
                  ])
              }
          }
          

          Jesse Glick added a comment - No idea offhand, would need to spend time digging into what Groovy is trying to call and why the sandbox interceptor is misinterpreting that. In general I do not advise use of the closure-delegate idiom in Pipeline libraries. It is subtle and easy to get wrong. Better to have a plain old method taking positional or named (~ map) arguments, some of which may be of type Closure . Something like (untested): def call(Closure body, String mailingList = 'builds-list@example.com' ) { catchError { body() } (currentBuild.result != 'ABORTED' ) && node( 'master' ) { // send e-mail notifications for failed or unstable builds def to = emailextrecipients([ [$class: 'CulpritsRecipientProvider' ], [$class: 'DevelopersRecipientProvider' ], [$class: 'RequesterRecipientProvider' ], [$class: 'UpstreamComitterRecipientProvider' ] ]) + ' ' + mailingList step([$class: 'Mailer' , notifyEveryUnstableBuild: true , recipients: to ]) } }
          Jesse Glick made changes -
          Component/s New: script-security-plugin [ 18520 ]
          Component/s New: workflow-cps-plugin [ 21713 ]
          Component/s Original: pipeline [ 21692 ]
          Jesse Glick made changes -
          Summary Original: scriptsecurity violation running pipeline step in closure with a delegate New: Closure delegate idiom fails attempting to call .DefaultGroovyMethods.invokeMethod(Object, String, Object)

          I'm also running into this issue and it's easily reproducible without a pipeline library (although, that's where I ran into it initially).

          def myPipeline(body = {}) {
              def config = [
                  preBuild: { echo "bye $it" }
              ]
          
              body.resolveStrategy = Closure.DELEGATE_FIRST
              body.delegate = config
              body()
          
              config.preBuild?.call("world")
          }
          
          myPipeline()
          
          myPipeline {
              preBuild = { echo "hello $it" }
          }
          

          I'm expecting the script to output:

          bye world
          hello world

           Instead, we see "bye world" followed by an exception. 
          org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
          To work around this, and a possible pointer to the fix, is to use the this object. For example, this works:

          myPipeline {
              preBuild = { this.echo "hello $it" }
          }

          I believe this works because  this points to the enclosing class where all the pipeline "globals" are still in the enclosing class's Binding.

          Steve Prentice added a comment - I'm also running into this issue and it's easily reproducible without a pipeline library (although, that's where I ran into it initially). def myPipeline(body = {}) { def config = [ preBuild: { echo "bye $it" } ] body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config body() config.preBuild?.call( "world" ) } myPipeline() myPipeline { preBuild = { echo "hello $it" } } I'm expecting the script to output: bye world hello world  Instead, we see "bye world" followed by an exception.  org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object To work around this, and a possible pointer to the fix, is to use the this object. For example, this works: myPipeline { preBuild = { this .echo "hello $it" } } I believe this works because  this points to the enclosing class  where all the pipeline "globals" are still in the enclosing class's Binding.

          Andrew Bayer added a comment -

          In that example, at least, preBuild needs its delegate set as well - when I change to this:

              body.resolveStrategy = Closure.DELEGATE_FIRST
              body.delegate = config
              body()
          
              if (config.preBuild != null) {
                  config.preBuild.resolveStrategy = Closure.DELEGATE_FIRST
                  config.preBuild.delegate = this
              }
              config.preBuild?.call("world")
          

          ...it works.

          In the original example, I believe you need to change the setup to something more like the second example - passing in the closure to run as a key/value pair itself. I don't think you can effectively mix closures to execute and variables to "return" like that.

          Andrew Bayer added a comment - In that example, at least, preBuild needs its delegate set as well - when I change to this: body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config body() if (config.preBuild != null ) { config.preBuild.resolveStrategy = Closure.DELEGATE_FIRST config.preBuild.delegate = this } config.preBuild?.call( "world" ) ...it works. In the original example, I believe you need to change the setup to something more like the second example - passing in the closure to run as a key/value pair itself. I don't think you can effectively mix closures to execute and variables to "return" like that.

          abayer, thanks for the workaround/solution! Works as you noted.

          Steve Prentice added a comment - abayer , thanks for the workaround/solution! Works as you noted.
          Andrew Bayer made changes -
          Resolution New: Not A Defect [ 7 ]
          Status Original: Open [ 1 ] New: Resolved [ 5 ]

            Unassigned Unassigned
            llamahunter Richard Lee
            Votes:
            4 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated: