Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: Each fix is at the line the CodeQL alert flags, and none goes further than the alert needs.
BatchRemoteCache.resultLockis now a privatenew Object()instead of the interned"resultLock"literal.FrameworkUtil.getFrameworkVersion()isstatic synchronized, which pairs it with the already synchronizedsetFrameworkVersion. The(ClassLoader)overload is renamed toreadFrameworkVersion, and its only callers are the two inFrameworkUtilTests.
question (non-blocking): Should connector-framework-contract keep working for connectors that pin TestNG 7.5 or later? ContractITCase.createInstances now instantiates the TestNG-internal org.testng.internal.ObjectFactoryImpl on every call.
OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/ContractITCase.java:72, :39
At the base, ObjectFactoryImpl was referenced only from the nested ContractTestFactory, which nothing ever instantiated, so the class was never loaded. Line 72 now runs before the loop on every @Factory call. TestNG 7.5 moved the class to org.testng.internal.objects, and 7.10.2 removed org.testng.IObjectFactory as well. I compiled copies of the base and head versions of the method and ran them on TestNG 6.9.10, 7.5, 7.6.0 and 7.10.2. The base version took its fallback on all four. The head version threw NoClassDefFoundError on the three 7.x versions. The factory is still never used, because no class in DEFAULT_TEST_CLASSES has a public (String) constructor. No build in this repo runs ContractITCase, so the break only shows up in downstream connectors that override TestNG. If those connectors are meant to be supported, this should block the merge; if not, it can wait. Plain reflection does the same job without the internal class:
for (Class<?> testClass: getContractTestClasses(context)) {
try {
Object test = testClass.getConstructor(String.class).newInstance("");
injector.injectMembers(test);
result.add(test);
} catch (NoSuchMethodException e) {
result.add(injector.getInstance(testClass));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot instantiate " + testClass.getName(), e);
}
}Then the IObjectFactory and ObjectFactoryImpl imports can go.
issue (non-blocking): getGroovyScriptEngine() stores the engine in the field before initializeCustomizer() runs. If the customizer fails, the engine stays in the field without its customizations.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:797-800, :820-822
The field has to be set first, because getCustomizerClass() (:727) calls the getter again. But when the customizer script throws, initializeCustomizer() only wraps the exception and rethrows it; it never clears the field. From then on every caller takes the fast path at :786 and runs scripts on an engine whose customizer never ran. For example, ScriptedRESTConfiguration.getHttpClient then fails on a null initClosure and the original error is lost. A thread that reaches :786 while the first thread is still inside the customizer also gets the engine without its customizations. This bug existed before the PR, and volatile does not make it worse. It does mean that volatile alone does not make this lazy initialization correct. Clearing the field on failure fixes the failure case. The case of a concurrent thread needs a separate "customized" flag.
groovyScriptEngine =
new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);
try {
initializeCustomizer();
} catch (RuntimeException e) {
groovyScriptEngine = null;
throw e;
}suggestion (non-blocking): Read groovyScriptEngine into a local variable once in getGroovyScriptEngine().
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:786, :804, :668
The fast path checks the volatile field at :786 and reads it again at :804. Meanwhile release() sets it to null at :668 under a lock the fast path does not take. On connector-server shutdown, ConnectorServerImpl.stop eventually calls ScriptedConfiguration.release, and ConnectionListener.shutdown does not wait for in-flight requests to finish. A script evaluation still running at that moment can get null back and throw an NPE in evaluate at :744. The window is small, and the race existed before the PR. Reading the field once into a local is the usual way to write this pattern with volatile:
GroovyScriptEngine engine = groovyScriptEngine;
if (null == engine) {
synchronized (this) {
engine = groovyScriptEngine;
if (null == engine) {
final CompilerConfiguration compilerConfiguration =
new CompilerConfiguration(config);
compilerConfiguration.addCompilationCustomizers(getImportCustomizer(null));
final GroovyClassLoader loader =
new GroovyClassLoader(getParentLoader(), compilerConfiguration, true);
engine = new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);
groovyScriptEngine = engine;
initializeCustomizer();
}
}
}
return engine;09a1ed7 to
9aa4f8d
Compare
|
I rebased the branch onto current master with no conflicts and addressed all three points in 9aa4f8d. question: issue: the engine stays uncustomized after a customizer failure. Opened as #148, together with the concurrent-caller case you described. I fixed both here instead of only clearing the field on failure. suggestion: read the field once. Opened as #149 and fixed in the same change. The field is read once into a local on the fast path and once under the lock, so I also updated the PR description: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 points are fixed where they started.
ScriptedConfiguration.getGroovyScriptEngine()writes the volatile field only afterinitializeCustomizer()returns. The re-entrant call goes throughcustomizingGroovyScriptEngine, which is guarded by the monitor. A failed customizer publishes nothing, and no other thread can see an uncustomized engine.testFailedCustomizerIsRetriedOnTheNextCallfails on the previous code: the second call returned at the fast path andcallsstayed 1. It ran green in CI at 9aa4f8d (groovy-connector: 127 run, 0 failed).ContractITCase.createInstancesno longer referencesorg.testng.internal.
issue (non-blocking): A REST/CREST/SSH customizer that keeps failing leaks one more GroovyClassLoader on every call.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:814-823, :832-845; OpenICF-groovy-connector/src/main/groovy/org/forgerock/openicf/connectors/scriptedrest/ScriptedRESTConfiguration.groovy:189, OpenICF-groovy-connector/src/main/groovy/org/forgerock/openicf/connectors/scriptedcrest/ScriptedCRESTConfiguration.groovy:224, OpenICF-ssh-connector/src/main/groovy/org/forgerock/openicf/connectors/ssh/SSHConfiguration.groovy:387
When the customizer throws, nothing is published. Each later getGroovyScriptEngine() call, from evaluate, loadScript or getHttpClient, builds a new loader and engine and runs the customizer again. That retry is intended. The REST, CREST and SSH createCustomizerScript overrides, though, run customizerClass.metaClass.customize << {…} before the script runs. That registers an ExpandoMetaClass on the newly compiled class, and on Groovy 2.4.21 the registration keeps the class's loader alive. In a probe of 50 failed attempts, every loader was still reachable after the heap was exhausted when the EMC was registered, and none was without it. So a customizer that fails at run time, for example a typo that raises MissingPropertyException, leaks one loader per failed operation. Before this change it was one per engine. GroovyClassLoader.clearCache() in 2.4.21 only clears its maps. InvokerHelper.removeClass unregisters the class. The fix below was not run.
private void initializeCustomizer() {
Class customizerClass = null;
try {
customizerClass = getCustomizerClass();
if (null != customizerClass) {
Binding binding = new Binding();
binding.setVariable(LOGGER, getLogger(customizerClass));
createCustomizerScript(customizerClass, binding).run();
}
} catch (Throwable t) {
if (null != customizerClass) {
// The retry compiles a new class; unregister this one so its loader can be collected.
InvokerHelper.removeClass(customizerClass);
}
logger.error(t, "Failed to customize the connector");
throw ConnectorException.wrap(t);
}
}Plus import org.codehaus.groovy.runtime.InvokerHelper;.
suggestion (non-blocking): Document that a customizer must not wait for another thread that uses this configuration's engine.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:798-805
Only the thread that holds the monitor sees customizingGroovyScriptEngine. Every other thread blocks on synchronized (this) until the customizer returns, which is what #148 asks for. One consequence: a customizer that runs Thread.start { configuration.loadScript('X.groovy') }.join(), or waits on an evaluate() it submitted, now hangs forever. The previous code published the engine early, so the same script finished. No bundled customizer does this.
/*
* This must be called once from thread-safe location and inside the
* synchronized to avoid deadlock. The customizer runs while this
* configuration's monitor is held: it must not wait for another thread
* that calls getGroovyScriptEngine(), evaluate() or loadScript().
*/
private void initializeCustomizer() {suggestion (non-blocking): testEngineIsHiddenFromOtherThreadsUntilCustomized takes a 500 ms timeout as proof that the second caller blocked.
OpenICF-groovy-connector/src/test/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfigurationTest.java:114-120
Suppose the engine were published early again. The second caller would then return as soon as it is scheduled, and that return is the only thing that turns this test red. If the pool thread is not scheduled within 500 ms on a loaded runner, the TimeoutException is caught, both futures later return the same engine, and isSameAs passes. On a normal schedule the test catches the regression, but not on every run. HEAD itself cannot fail spuriously.
final AtomicReference<Thread> secondThread = new AtomicReference<>();
Future<GroovyScriptEngine> second = executor.submit(() -> {
secondThread.set(Thread.currentThread());
return configuration.getGroovyScriptEngine();
});
while (!second.isDone() && (secondThread.get() == null
|| secondThread.get().getState() != Thread.State.BLOCKED)) {
Thread.sleep(10);
}
assertThat(second.isDone()).as("second caller returned while the customizer ran").isFalse();Pin: this replaces lines 114-120, plus import java.util.concurrent.atomic.AtomicReference;. An early publish then fails on every run, not only when the pool thread is scheduled within 500 ms.
…Platform#132, align the 3A copyright lines
9aa4f8d to
9ce4b58
Compare
|
I rebased the branch onto current master with no conflicts and addressed all three points in 9ce4b58. issue: loader leak on a failing customizer. suggestion: document the deadlock constraint. Added your wording to the comment on suggestion: the 500 ms timeout. Replaced it with your loop: the test waits until the second thread is I checked the three tests against a reverted fix (no |
…nterned-string locks, racy lazy init SQLUtil tested "instanceof Integer" twice, the second branch unreachable. ContractITCase dereferenced a factory that was always null; it now uses TestNG's ObjectFactoryImpl, and the unused nested ContractTestFactory goes. BatchRemoteCache synchronised on an interned string literal, a monitor shared with any other code using the same literal; it locks a private object now. The double-checked lazy initialisation of WebSocketConnectionGroup.operationContext, ScriptedConfiguration.groovyScriptEngine and TstStatefulConnectorConfig.executorService reads a non-volatile field outside the lock; the fields are volatile now. FrameworkUtil's version getter is synchronised like its setter, and the class-loader overload, which reads a resource rather than the field, is readFrameworkVersion.
…tNG internals from ContractITCase ScriptedConfiguration.getGroovyScriptEngine() stored the engine before initializeCustomizer() ran: a failed customizer left an uncustomized engine in place for good, and a concurrent caller could get it mid-customization (OpenIdentityPlatform#148). The fast path also read the volatile field twice while release() may null it in between (OpenIdentityPlatform#149). The field is now read once, and the engine is published only after the customizer succeeds; the customizer's own re-entrant calls get it from a lock-guarded field. ContractITCase.createInstances instantiated org.testng.internal.ObjectFactoryImpl on every call; TestNG 7.5 moved that class and 7.10.2 removed IObjectFactory. It now calls the (String) constructor through plain reflection. Fixes OpenIdentityPlatform#148 Fixes OpenIdentityPlatform#149
… in the test Since a failed customizer is now retried on the next call, each failure compiled a new customizer class. The REST, CREST and SSH configurations register an ExpandoMetaClass on it, which keeps its GroovyClassLoader alive, so a customizer that keeps failing leaked one loader per call. initializeCustomizer() now removes the class with InvokerHelper.removeClass when the customizer fails. The comment on initializeCustomizer() states that the customizer runs under the configuration's monitor and must not wait for another thread that uses the engine. testEngineIsHiddenFromOtherThreadsUntilCustomized took a 500 ms timeout as proof that the second caller blocked; it now waits until that thread is BLOCKED or done, so an early publish fails the test on every run.
9ce4b58 to
863f079
Compare
|
Right after my previous comment #133, #134, #136, #141 and #143 were merged, and the branch conflicted with master again. I rebased it once more; the head is now 863f079. The only conflict was in On the new base I checked the three tests against reverted parts of the fix: engine published before the customizer, no |
Closes the remaining 10 open CodeQL alerts of severity
errorthat carry no security rating:java/contradictory-type-checks#1536 #1537,java/dereferenced-value-is-always-null#1560,java/sync-on-boxed-types#1554 #1555 #1556,java/unsafe-double-checked-locking#1550 #1551,java/unsynchronized-getter#1544 #1545. The eleventh, #1549 (ScriptedConfiguration), was closed by #133.Fixes #148
Fixes #149
SQLUtil.setParamelse if (val instanceof Integer)appears twice; the second branch can never runContractITCase.createInstancesIObjectFactory objectFactory = nullis dereferenced for any test class with a(String)constructor. None of the default classes has one today, so the code only worked because theNoSuchMethodExceptionpath was always taken(String)constructor is called through plain reflection, so the method no longer depends on TestNG's internalObjectFactoryImpl, which TestNG 7.5 moved and whoseIObjectFactory7.10.2 removed. The unused nestedContractTestFactorywith its three never-read fields is goneBatchRemoteCache(testbundlev1)synchronized (resultLock)on the interned literal"resultLock", a monitor shared with any other code in the JVM that synchronises on the same stringnew Object()WebSocketConnectionGroup.operationContext,TstStatefulConnectorConfig.executorServicevolatile, which makes the pattern correct under the JMMScriptedConfiguration.getGroovyScriptEngine()synchronizedfor the whole initialisation. That closed the double-checked-locking alert and #149 (the getter andrelease()share the monitor now), and concurrent callers wait for the customizer. But the getter still stored the engine before its customizer ran, so a failed customizer left an uncustomized engine in place for good (#148)initializeCustomizer()succeeds. The customizer's own re-entrant calls get the engine under construction from a second lock-guarded field. If the customizer fails, nothing is published and the next call retries; the failed customizer class is removed withInvokerHelper.removeClass, so the metaclass the REST, CREST and SSH configurations register on it does not keep one class loader alive per retry. The customizer runs under the configuration's monitor, so it must not wait for another thread that uses the engine; the comment oninitializeCustomizer()says soFrameworkUtil.getFrameworkVersion()setFrameworkVersionis synchronisedstatic synchronized(not a hot path). The(ClassLoader)overload readsconnectors-framework.propertiesrather than the field and is only used byFrameworkUtilTests, so it is renamed toreadFrameworkVersionand the getter/setter pairing no longer applies to itScriptedConfigurationTestcovers #148: a customizer that fails once is retried on the next call, a second thread blocks until the customizer has finished, and a failed customizer class has no metaclass left registered. Each test fails when its part of the fix is reverted (engine published before the customizer, noremoveClass, getter not synchronised). #149 is fixed on master by #133; it was a window between two adjacent field reads that no test can hold open, so it has no test. The other changes have no behaviour a unit test can observe: dead branches,volatile, a private monitor and a synchronised getter.createInstancestakes its classes from a fixed list with no(String)constructors, so its reflective path cannot be reached from a test.Local reactor run (rebased on current master, after #133 #134 #136 #141 #143) of all touched modules and everything they depend on: connector-framework 197, dbcommon 86, framework-internal 509 (2 skipped), contract 43, connector-framework-server 32, groovy-connector 129 (42 skipped as on master, 3 new), ssh-connector 96 (82 skipped), testbundlev1 compiles — all green.