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

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

    XMLWordPrintable

    Details

    • Similar Issues:

      Description

      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
      

        Attachments

          Issue Links

            Activity

            Hide
            llamahunter 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.

            Show
            llamahunter 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.
            Hide
            jglick 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
                    ])
                }
            }
            
            Show
            jglick 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 ]) } }
            Hide
            steveprentice 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.

            Show
            steveprentice 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.
            Hide
            abayer 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.

            Show
            abayer 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.
            Hide
            steveprentice Steve Prentice added a comment -

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

            Show
            steveprentice Steve Prentice added a comment - Andrew Bayer , thanks for the workaround/solution! Works as you noted.
            Hide
            pixman20 pixman20 added a comment - - edited

            Andrew Bayer, I don't think the original issue in the description was resolved, only the later example in the comments.

            In the original example attached (copied below), the "sh" line is getting this error as shown in the description

            @Library('myPipelineLib') _
            
            emailBreaks {
                mailingList = 'user@example.com'
                
                node('master') {
                    git 'https://git.example.org/user/pipeline-build-failures.git'
                    //sh 'true'
                    sh 'might fail' // <-- Causes: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
                }
            }
            

            Looking at the emailBreaks shared pipeline function, I'm not sure how your previous solution applies (I could easily just be missing it)

            def call(Closure body) {
                currentBuild.result = 'SUCCESS'
            
                def config = [:]
                body.resolveStrategy = Closure.DELEGATE_FIRST
                body.delegate = config
                
                catchError {
                    body() // <-- Running the closure here delegates to config, but seems to make anything available in the owner's scope a script approval problem
                }
            
                ...
            }
             

            I have the exact same situation as this, error and all.
            I can workaround it (painfully so), by explicitly defining the object containing the function that's being invoked.
            For example, using the above example, changing "sh" to either "this.sh" or "steps.sh" would work probably work (it was fixed in my case by adding the global library name)
            For some reason, when letting the scope with the defined function be found implicitly, the script approval part gets angry

            Show
            pixman20 pixman20 added a comment - - edited Andrew Bayer , I don't think the original issue in the description was resolved, only the later example in the comments. In the original example attached (copied below), the "sh" line is getting this error as shown in the description @Library( 'myPipelineLib' ) _ emailBreaks { mailingList = 'user@example.com' node( 'master' ) { git 'https: //git.example.org/user/pipeline-build-failures.git' //sh ' true ' sh 'might fail' // <-- Causes: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang. Object java.lang. String java.lang. Object } } Looking at the emailBreaks shared pipeline function, I'm not sure how your previous solution applies (I could easily just be missing it) def call(Closure body) { currentBuild.result = 'SUCCESS' def config = [:] body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config catchError { body() // <-- Running the closure here delegates to config, but seems to make anything available in the owner's scope a script approval problem } ... }   I have the exact same situation as this, error and all. I can workaround it (painfully so), by explicitly defining the object containing the function that's being invoked. For example, using the above example, changing "sh" to either "this.sh" or "steps.sh" would work probably work (it was fixed in my case by adding the global library name) For some reason, when letting the scope with the defined function be found implicitly, the script approval part gets angry
            Hide
            thefifo Wolfgang Issovits added a comment -

            The original issue is definitely not resolved. The example from pixman is still not working which indicates that the resolveStrategy is not handled correctly for pipelines / libraries.

            Show
            thefifo Wolfgang Issovits added a comment - The original issue is definitely not resolved. The example from pixman is still not working which indicates that the resolveStrategy is not handled correctly for pipelines / libraries.

              People

              Assignee:
              Unassigned Unassigned
              Reporter:
              llamahunter Richard Lee
              Votes:
              2 Vote for this issue
              Watchers:
              7 Start watching this issue

                Dates

                Created:
                Updated: