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

Overhauled ProxyWhitelist #549

Merged
merged 4 commits into from
Jan 3, 2024
Merged

Overhauled ProxyWhitelist #549

merged 4 commits into from
Jan 3, 2024

Conversation

jglick
Copy link
Member

@jglick jglick commented Dec 19, 2023

Fixes jenkinsci/acceptance-test-harness#1444. (I guess; I do not care to run ATH locally, but anyway the problenatic code is completely rewritten so it ought to be possible to revert jenkinsci/acceptance-test-harness#1445.) Motivated by #545 (comment): there was some code that attempted to work around Guice cycles, but I guess it behaved nondeterministically depending on extension load order. Simplified the whole thing, which required rewriting ProxyWhitelist to be much simpler and not try to cache anything, and rewriting EnumeratingWhitelist to use a simpler cache system (replacing #180 and various other accreted patches).

@jglick jglick requested a review from a team as a code owner December 19, 2023 19:44
@jglick jglick added the bug label Dec 19, 2023

@Override public final boolean permitsMethod(@NonNull Method method, @NonNull Object receiver, @NonNull Object[] args) {
String key = canonicalMethodSig(method);
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that this will no longer cache results when there is a distinct Method with the same signature, which could happen when there is a new copy of the Class from a distinct ClassLoader. However https://github.com/jenkinsci/workflow-cps-plugin/blob/3a59e40fb41d2774421ce3d7c00480e5917edb76/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/GroovyClassLoaderWhitelist.java#L45 consults its delegate https://github.com/jenkinsci/workflow-cps-plugin/blob/3a59e40fb41d2774421ce3d7c00480e5917edb76/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsGroovyShell.java#L181 last so I do not think that would normally happen. Once the system warms up, the normal path through ought to be fast. Would be useful to have some JMH benchmarks if they can run via RealJenkinsRule.

Copy link
Member

@timja timja left a comment

Choose a reason for hiding this comment

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

Looks a lot simpler

@jglick
Copy link
Member Author

jglick commented Dec 20, 2023

I would like to do some sort of performance sanity check of this. Also, it is probably not the best idea to release a rewrite like this right before taking time off!

@jglick
Copy link
Member Author

jglick commented Dec 20, 2023

Installing workflow-aggregator + deps from setup wizard before this PR throws an error, FTR, which I guess corresponds to the ATH problems:

Log
WARNING	h.ExtensionFinder$GuiceFinder$FaultTolerantScope$1#error: Failed to instantiate Key[type=org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist, annotation=[none]]; skipping this component
java.lang.IllegalStateException: maybe need to rebuild plugin?
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.get(ScriptApproval.java:137)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist.reconfigure(ScriptApproval.java:991)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist.<init>(ScriptApproval.java:984)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist$$FastClassByGuice$$1780ecba.GUICE$TRAMPOLINE(<generated>)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist$$FastClassByGuice$$1780ecba.apply(<generated>)
	at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:82)
	at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:114)
	at com.google.inject.internal.ConstructorInjector.access$000(ConstructorInjector.java:33)
	at com.google.inject.internal.ConstructorInjector$1.call(ConstructorInjector.java:98)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:109)
	at hudson.ExtensionFinder$GuiceFinder$SezpozModule.onProvision(ExtensionFinder.java:610)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:117)
	at hudson.ExtensionFinder$GuiceFinder$SezpozModule.onProvision(ExtensionFinder.java:610)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:117)
	at com.google.inject.internal.ProvisionListenerStackCallback.provision(ProvisionListenerStackCallback.java:66)
	at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:93)
	at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:300)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
Caused: com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) [Guice/ErrorInjectingConstructor]: IllegalStateException: maybe need to rebuild plugin?
  at ScriptApproval$ApprovedWhitelist.<init>(ScriptApproval.java:982)

Learn more:
  https://github.com/google/guice/wiki/ERROR_INJECTING_CONSTRUCTOR

1 error

======================
Full classname legend:
======================
ScriptApproval$ApprovedWhitelist: "org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist"
========================
End of classname legend:
========================

	at com.google.inject.internal.InternalProvisionException.toProvisionException(InternalProvisionException.java:251)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:43)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:169)
	at hudson.ExtensionFinder$GuiceFinder$FaultTolerantScope$1.get(ExtensionFinder.java:445)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:45)
	at com.google.inject.internal.InjectorImpl$1.get(InjectorImpl.java:1148)
	at hudson.ExtensionFinder$GuiceFinder._find(ExtensionFinder.java:403)
	at hudson.ExtensionFinder$GuiceFinder.find(ExtensionFinder.java:394)
	at hudson.ClassicPluginStrategy.findComponents(ClassicPluginStrategy.java:344)
	at hudson.ExtensionList.load(ExtensionList.java:384)
	at hudson.ExtensionList.ensureLoaded(ExtensionList.java:320)
	at hudson.ExtensionList.iterator(ExtensionList.java:172)
	at hudson.ExtensionList.get(ExtensionList.java:149)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.reconfigure(ScriptApproval.java:1139)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.load(ScriptApproval.java:540)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.<init>(ScriptApproval.java:506)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$$FastClassByGuice$$17796193.GUICE$TRAMPOLINE(<generated>)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$$FastClassByGuice$$17796193.apply(<generated>)
	at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:82)
	at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:114)
	at com.google.inject.internal.ConstructorInjector.access$000(ConstructorInjector.java:33)
	at com.google.inject.internal.ConstructorInjector$1.call(ConstructorInjector.java:98)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:109)
	at hudson.ExtensionFinder$GuiceFinder$SezpozModule.onProvision(ExtensionFinder.java:610)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:117)
	at hudson.ExtensionFinder$GuiceFinder$SezpozModule.onProvision(ExtensionFinder.java:610)
	at com.google.inject.internal.ProvisionListenerStackCallback$Provision.provision(ProvisionListenerStackCallback.java:117)
	at com.google.inject.internal.ProvisionListenerStackCallback.provision(ProvisionListenerStackCallback.java:66)
	at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:93)
	at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:300)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:169)
	at hudson.ExtensionFinder$GuiceFinder$FaultTolerantScope$1.get(ExtensionFinder.java:445)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:45)
	at com.google.inject.internal.InjectorImpl$1.get(InjectorImpl.java:1148)
	at hudson.ExtensionFinder$GuiceFinder._find(ExtensionFinder.java:403)
	at hudson.ExtensionFinder$GuiceFinder$3.find(ExtensionFinder.java:357)
	at jenkins.ExtensionComponentSet$3.find(ExtensionComponentSet.java:98)
	at jenkins.ExtensionComponentSet$1.find(ExtensionComponentSet.java:70)
	at hudson.ExtensionList.load(ExtensionList.java:391)
	at hudson.ExtensionList.refresh(ExtensionList.java:345)
	at jenkins.model.Jenkins.refreshExtensions(Jenkins.java:2923)
	at hudson.PluginManager.start(PluginManager.java:990)
	at hudson.model.UpdateCenter$CompleteBatchJob.run(UpdateCenter.java:2367)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at hudson.remoting.AtmostOneThreadExecutor$Worker.run(AtmostOneThreadExecutor.java:121)
	at java.base/java.lang.Thread.run(Thread.java:829)

@jglick
Copy link
Member Author

jglick commented Dec 20, 2023

Set up a controller running 3872d58 with 10 (mock agent) executors and a pipeline with a string parameter x

library 'github.com/jglick/sample-pipeline-library'
def changes = currentBuildExt().hasChangeIn('xxx')
echo """
Hello at ${new Date()}!
Triggered as $x
Do we have any changes? $changes
"""
node {
    parallel main: {
        stage('Build') {
            withMockLoad(averageDuration: 3) {
                sh MOCK_LOAD_COMMAND
            }
        }
        stage('Results') {
            junit 'mock-junit.xml'
            archiveArtifacts 'mock-artifact-*.txt'
        }
    }, stuff: {
        sh 'sleep 3'
        echo(/hello from ${Jenkins.instance}/) // approved
    }
}
def r = new Random()
['a', 'b'].each {one ->
    def p = r.nextInt(20)
    echo "$one triggering at $p"
    build job: JOB_NAME, wait: false, parameters: [string(name: 'x', value: String.valueOf(p))]
}

I saw no evidence of classes from this plugin in thread dumps, other than momentarily while processing the build step; the term whitelist did not even appear. Checked a flame view in JMC and limited view to CpsThread.runNextChunk (<10% of the time I guess). Some activity of SandboxInterceptor and GroovyCallSiteSelector but very little related to this PR; had to zoom way in to even see calls to permitsMethod or permitsStaticMethod.

private final Map<ProxyWhitelist,Void> wrappers = new WeakHashMap<>();

// TODO Consider StampedLock when we switch to Java8 for better performance - https://dzone.com/articles/a-look-at-stampedlock
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Copy link
Member

@dwnusbaum dwnusbaum Jan 2, 2024

Choose a reason for hiding this comment

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

FWIW, I think the old code would have blocked permits* calls from other threads while ApprovedWhitelist was being reconfigured, while the new code with the lock removed reconfigures things on the first thread that sees ApprovedWhitelist.initialized change from false to true, and other threads will not be blocked and will either use the old or new list depending on timing. Seems ok to me, but I just wanted to note the behavior change.

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, I think the lock was there merely to prevent momentary inconsistencies, not specifically to block permit* calls.

@jglick
Copy link
Member Author

jglick commented Jan 3, 2024

Had a lot of problems running ATH due to mock UC issues, e.g.

java.io.IOException: Downloaded file …/acceptance-test-harness/target/jenkinshome14266535027719496839/plugins/sshd.jpi.tmp does not match expected SHA-256, expected 'zmwrQMBV2tXA2H7A+3n5IRgNAI4HxbtjDksHcr/d68E=', actual '7sJzkPL0yiMgeTLCnThwKtojtDK6WKuNwUTYgj6sFzU='

Had to manually delete local repository files. Finally managed to get ScriptSecurityPluginTest.signatureNeedsApproval to fail in the expected way, first giving an error during dynamic plugin installation

Guice error
2024-01-03 13:19:33.487+0000 [id=94]	WARNING	h.ExtensionFinder$GuiceFinder$FaultTolerantScope$1#error: Failed to instantiate Key[type=org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist, annotation=[none]]; skipping this component
java.lang.IllegalStateException: maybe need to rebuild plugin?
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.get(ScriptApproval.java:137)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist.reconfigure(ScriptApproval.java:991)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist.<init>(ScriptApproval.java:984)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist$$FastClassByGuice$$15e5e64e.GUICE$TRAMPOLINE(<generated>)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist$$FastClassByGuice$$15e5e64e.apply(<generated>)
	at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:82)
	at …
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
Caused: com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) [Guice/ErrorInjectingConstructor]: IllegalStateException: maybe need to rebuild plugin?
  at ScriptApproval$ApprovedWhitelist.<init>(ScriptApproval.java:982)

Learn more:
  https://github.com/google/guice/wiki/ERROR_INJECTING_CONSTRUCTOR

1 error

======================
Full classname legend:
======================
ScriptApproval$ApprovedWhitelist: "org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$ApprovedWhitelist"
========================
End of classname legend:
========================

	at com.google.inject.internal.InternalProvisionException.toProvisionException(InternalProvisionException.java:251)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:43)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:169)
	at hudson.ExtensionFinder$GuiceFinder$FaultTolerantScope$1.get(ExtensionFinder.java:445)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:45)
	at com.google.inject.internal.InjectorImpl$1.get(InjectorImpl.java:1148)
	at hudson.ExtensionFinder$GuiceFinder._find(ExtensionFinder.java:403)
	at hudson.ExtensionFinder$GuiceFinder.find(ExtensionFinder.java:394)
	at hudson.ClassicPluginStrategy.findComponents(ClassicPluginStrategy.java:344)
	at hudson.ExtensionList.load(ExtensionList.java:384)
	at hudson.ExtensionList.ensureLoaded(ExtensionList.java:320)
	at hudson.ExtensionList.iterator(ExtensionList.java:172)
	at hudson.ExtensionList.get(ExtensionList.java:149)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.reconfigure(ScriptApproval.java:1139)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.load(ScriptApproval.java:540)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.<init>(ScriptApproval.java:506)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$$FastClassByGuice$$15dff929.GUICE$TRAMPOLINE(<generated>)
	at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval$$FastClassByGuice$$15dff929.apply(<generated>)
	at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:82)
	at …
	at hudson.ExtensionList.load(ExtensionList.java:391)
	at hudson.ExtensionList.refresh(ExtensionList.java:345)
	at jenkins.model.Jenkins.refreshExtensions(Jenkins.java:2923)
	at hudson.PluginManager.start(PluginManager.java:990)
	at hudson.model.UpdateCenter$CompleteBatchJob.run(UpdateCenter.java:2367)
	at …

and then failing in the test proper

Assertion failure
java.lang.AssertionError: 

Expected: Build result SUCCESS
     but: was FAILURE. Console output:
Started by user user
Running as SYSTEM
Building in workspace …/acceptance-test-harness/target/jenkinshome13884859041180232262/workspace/main_rainbow
ERROR: Failed to evaluate groovy script.
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod java.lang.System getProperties
 at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:243)
 at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onStaticCall(SandboxInterceptor.java:212)
 at org.kohsuke.groovy.sandbox.impl.Checker$2.call(Checker.java:214)
 at org.kohsuke.groovy.sandbox.impl.Checker.checkedStaticCall(Checker.java:218)
 at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:120)
 at org.kohsuke.groovy.sandbox.impl.Checker$checkedCall.callStatic(Unknown Source)
 at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)
 at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:197)
 at Script1.run(Script1.groovy:1)
 at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox.runScript(GroovySandbox.java:195)
 at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript.evaluate(SecureGroovyScript.java:366)
 at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript.evaluate(SecureGroovyScript.java:310)
 at org.jvnet.hudson.plugins.groovypostbuild.GroovyPostbuildRecorder.perform(GroovyPostbuildRecorder.java:434)
 at hudson.tasks.BuildStepMonitor$1.perform(BuildStepMonitor.java:20)
 at hudson.model.AbstractBuild$AbstractBuildExecution.perform(AbstractBuild.java:818)
 at hudson.model.AbstractBuild$AbstractBuildExecution.performAllBuildSteps(AbstractBuild.java:767)
 at hudson.model.Build$BuildExecution.post2(Build.java:179)
 at hudson.model.AbstractBuild$AbstractBuildExecution.post(AbstractBuild.java:711)
 at hudson.model.Run.execute(Run.java:1918)
 at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:44)
 at hudson.model.ResourceController.execute(ResourceController.java:101)
 at hudson.model.Executor.run(Executor.java:442)
Build step 'Groovy Postbuild' changed build result to FAILURE
Build step 'Groovy Postbuild' marked build as failure
Finished: FAILURE
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:6)
	at org.jenkinsci.test.acceptance.po.Build.shouldSucceed(Build.java:214)
	at plugins.ScriptSecurityPluginTest.shouldSucceed(ScriptSecurityPluginTest.java:100)
	at plugins.ScriptSecurityPluginTest.signatureNeedsApproval(ScriptSecurityPluginTest.java:126)

which is fixed using LOCAL_JARS pointing to this PR.

@jglick jglick merged commit 0079bcd into jenkinsci:master Jan 3, 2024
14 checks passed
@jglick jglick deleted the ProxyWhitelist branch January 3, 2024 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

All tests that touch script security approval are flakey
4 participants