Fix genuine bugs found while cleaning up java/unused-parameter alerts - #141
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The three real bugs are fixed where they live, and the new tests go red against the old code.
ADUserAccountControl's private constructor now storesmsDSUacin its own field, soisAccountLockOut()/isPasswordExpired()finally readms-DS-User-Account-Control-Computed.- Both JS executors restore the previous TCCL in
finally(JavaScriptExecutorFactory.java:112-116,:140-144), so a throwing script does not leak the caller's loader into a pooled thread. UnauthorizedResponseTestfails against the baseOpenICFWebSocketCreatorand passes at head.
question (non-blocking): Should a null loader keep the ambient thread context classloader, as it did before this PR?
OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactory.java:111, :139
Both executors call setContextClassLoader(loader) without a null check, and ScriptExecutorFactory.newScriptExecutor does not forbid null. testbundlev1's TstAbstractConnector.runScriptOnResource (:375) passes null with compile=true. At head, that script runs with TCCL = null instead of the bundle loader that ThreadClassLoaderManagerProxy set. Under Rhino 1.7.15, Packages.<bundle-only class> then resolves to a package object, not the class (measured: base "function", head "object"). On the legacy connector server, whose workers are CCLWatchThreads, every such run also logs ERROR Attempting to set the CCL of thread ... to null with a stack trace (CCLWatchThread.java:56-60). This is Minor whatever the answer: no production caller passes null. In this repo only the test bundle does, and a GitHub code search over the ConnId/Evolveum/ForgeRock/WrenSecurity connectors found none. If the answer is yes, guard both executors:
// CompiledJavaScriptExecutor.execute; the same in JavaScriptExecutor.execute around engine.eval(script)
if (loader == null) {
return compiled.eval(newContext);
}
Thread currentThread = Thread.currentThread();
ClassLoader previousLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(loader);
try {
return compiled.eval(newContext);
} finally {
currentThread.setContextClassLoader(previousLoader);
}Pin: newScriptExecutor(null, "java.lang.Thread.currentThread().getContextClassLoader();", true).execute(null) must return the test thread's TCCL. At head it returns null.
suggestion (non-blocking): No test pins the TCCL restore on the compiled path or after a throwing script.
OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactoryTests.java:59-65, :51-55
The only post-execute check, testScriptDoesNotLeakClassLoaderAfterExecution, uses compile=false. testCompiledScriptRunsWithProvidedClassLoader reads the TCCL only during eval, and every test script finishes normally. Two mutants keep all 5 tests green: deleting the restore from CompiledJavaScriptExecutor.execute's finally (JavaScriptExecutorFactory.java:115), and replacing try/finally with set-eval-restore in either executor (traced, not run). Either change leaks the custom loader into the calling thread.
@Test
public void testCompiledScriptDoesNotLeakClassLoaderAfterExecution() throws Exception {
ClassLoader before = Thread.currentThread().getContextClassLoader();
ClassLoader custom = new URLClassLoader(new java.net.URL[0], getClass().getClassLoader());
getScriptExecutor("1;", custom, true).execute(null);
assertSame(Thread.currentThread().getContextClassLoader(), before);
}
@Test
public void testFailingScriptDoesNotLeakClassLoader() throws Exception {
ClassLoader before = Thread.currentThread().getContextClassLoader();
ClassLoader custom = new URLClassLoader(new java.net.URL[0], getClass().getClassLoader());
try {
getScriptExecutor("throw 'boom';", custom, true).execute(null);
org.testng.Assert.fail("the script must throw");
} catch (Exception expected) {
// ScriptException from eval
}
assertSame(Thread.currentThread().getContextClassLoader(), before);
}Pin: the first case kills the missing-restore mutant on the compiled path. The second kills the set-eval-restore mutant.
suggestion (non-blocking): UnauthorizedResponseTest does not pin that unauthorized() forwards its message, nor the 403.
OpenICF-java-framework/connector-server-jetty/src/test/java/org/forgerock/openicf/framework/server/jetty/UnauthorizedResponseTest.java:87-98
The test goes through the single caller (OpenICFWebSocketCreator.java:133), which always passes the literal "Unknown Principal". It asserts only contains("Unknown Principal"), and the sendError handler ignores a[0]. Two mutants of unauthorized() (:166) stay green: one that hardcodes "Unknown Principal: a client certificate ..." and drops message, and one with any status other than SC_FORBIDDEN (traced, not run). The test's own Javadoc promises "the specific reason it was passed".
@Test
public void testUnauthorizedForwardsTheGivenReasonWith403() throws Exception {
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);
try {
OpenICFWebSocketCreator creator = new OpenICFWebSocketCreator(null, noopListener(),
new Authenticator() {
@Override
public void authenticate(JettyServerUpgradeRequest request,
JettyServerUpgradeResponse response, NameCallback callback) {
}
}, scheduler);
final Object[] sent = new Object[2];
JettyServerUpgradeResponse response = (JettyServerUpgradeResponse) Proxy.newProxyInstance(
UnauthorizedResponseTest.class.getClassLoader(),
new Class<?>[] { JettyServerUpgradeResponse.class },
new InvocationHandler() {
public Object invoke(Object p, Method m, Object[] a) {
if ("sendError".equals(m.getName())) {
sent[0] = a[0];
sent[1] = a[1];
}
return null;
}
});
creator.unauthorized(response, "Key mismatch");
Assert.assertEquals(sent[0], 403);
Assert.assertTrue(((String) sent[1]).startsWith("Key mismatch: "));
} finally {
scheduler.shutdownNow();
}
}Pin: a reason other than the one production literal kills the hardcoding mutant, and the sent[0] check kills the status-code one.
…loader, pin the restore and the forwarded reason - JavaScriptExecutorFactory: leave the thread context classloader untouched when no loader is requested, as GroovyShell does for a null parent - Test the TCCL restore on the compiled path and after a throwing script - Test that unauthorized() forwards its reason with a 403
b94a5ec to
fe0e461
Compare
|
All three points are addressed in fe0e461; the branch is also rebased onto current null loader (question): yes. Both executors now leave the thread context classloader untouched when TCCL restore tests: added
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 feedback landed where it was aimed.
- Both executors now return
eval()directly for anullloader (JavaScriptExecutorFactory.java:111-112,:142-143), so the caller's TCCL is kept;testNullClassLoaderKeepsTheCallersContextClassLoadergoes red on the oldsetContextClassLoader(null). UnauthorizedResponseTestcallsunauthorized(response, reason)directly and assertsSC_FORBIDDENand the forwarded reason.testCompiledScriptDoesNotLeakClassLoaderAfterExecutionandtestFailingScriptDoesNotLeakClassLoadercover the compiled path and a throwing script.
- ADUserAccountControl's private constructor assigned uac to both fields, so isAccountLockOut()/isPasswordExpired() never read the real msDSUac value from AD's ms-DS-User-Account-Control-Computed attribute. - JavaScriptExecutorFactory accepted a ClassLoader but never applied it, unlike its Groovy sibling; JS scripts always ran under the ambient thread context classloader instead of the one requested by the caller. - OpenICFWebSocketCreator.unauthorized() dropped the specific reason it was passed and always sent the same generic explanation. Also removes dead OperationOptions/typeName parameters from private helpers (ActiveDirectoryChangeLogSyncStrategy.handleEvents, SchemaApiOpTests.getTestPropertyOrFail, CSVFileConnector findAccount/doDelete/doUpdate) and updates their call sites.
…loader, pin the restore and the forwarded reason - JavaScriptExecutorFactory: leave the thread context classloader untouched when no loader is requested, as GroovyShell does for a null parent - Test the TCCL restore on the compiled path and after a throwing script - Test that unauthorized() forwards its reason with a 403
fe0e461 to
f5156f2
Compare
|
Rebased onto current The only conflict was the license header of |
## Summary Follow-up to the unused-parameter cleanup (#141): investigated all the newly-surfaced small `note`-severity CodeQL categories. - **`GuardedString`** now overrides `toString()` (returns `"GuardedString(...)"`), so it no longer inherits `Object`'s default. Fixes `call-to-object-tostring` at its root instead of patching the two current call sites (`SharedSecretPrincipal`, `ScriptOnResourceApiOpTests`) individually — any future logging of a `GuardedString` is safe by construction. This is more than cosmetic: the inherited output printed `hashCode()`, which is derived from the unsalted SHA-1 hash of the clear text, so a fingerprint of the secret reached log text — through `SharedSecretPrincipal.toString()`, and, with OK-level logging on, through `LoggingProxy`, which prints every API argument and return value: a `GuardedString` attribute value (e.g. `__PASSWORD__`) passed to create/update or returned by getObject was printed via `Attribute.toString()`. - **`GuardedByteArray`** gets the same override (`"GuardedByteArray(...)"`): its `hashCode()` is derived from the clear bytes the same way, and it is a supported attribute type, so the same `LoggingProxy` → `Attribute.toString()` path printed it. - Fixes a shadowed local in `LdapInternalSearch.execute()` (`local-shadows-field`). Also dismissed on GitHub: 3 `ignored-error-status-of-call` (the ignored return values are already covered by a subsequent check or exception path), 8 `jdk-internal-api-access` (AD DirSync's `com.sun.jndi.ldap.Ber*`, no public JDK alternative exists), 1 `confusing-method-signature` (`Log.log(..)` is long-standing, heavily-used public API — renaming to remove the overload is too invasive for the benefit). Covered by other PRs instead of this one: - #134 (opened independently, earlier) fixes byte-for-byte the same lines in `AttributeTypeUtil.java`, `MultiOpTests.java`, `PrettyStringBuilder.java`, `StringUtil.java`, `ActiveDirectoryChangeLogSyncStrategy.java`, `XSDAnnotationParser.java` (with `AttributeTypeUtilTests.java`): `uncaught-number-format-exception`, `inefficient-boxed-constructor`, `inefficient-empty-string-test`, `inefficient-key-set-iterator`, `unknown-javadoc-parameter`, `missing-space-in-concatenation`. Dropped here on 2026-09-21. - #132 removes the dead `ContractTestFactory` inner class from `ContractITCase` (`unused-reference-type`) along with more of that file's dead code; the two PRs conflicted there, so this PR no longer touches `ContractITCase`. Dropped here on 2026-10-04. **Update 2026-10-02 (review round 1):** rebased onto the current `master`; added the `GuardedByteArray` override; replaced the `toString()` test, which passed even without the override, by one that pins the fix; reworded the Javadoc to say why the override matters. **Update 2026-10-04 (review round 2):** rebased onto the current `master` (picks up #146, which fixes the `docs.groovy-lang.org` javadoc failure that turned the `windows-latest, 11` cell red); both `toString()` tests now also pin the exact output; corrected the description of the `GuardedByteArray` leak (it was reachable, not latent); dropped the `ContractITCase` change in favour of #132; aligned the added 3A copyright lines to the repository's `Portions Copyrighted 2026 3A Systems, LLC`. ## Test plan - [x] `GuardedStringTests.testToStringDoesNotDependOnTheSecret` and `GuardedByteArrayTests.testToStringDoesNotDependOnTheSecret`: two different secrets print the same `toString()`, and it is exactly `"GuardedString(...)"` / `"GuardedByteArray(...)"`. Verified red with the overrides removed (round 1) and with `toString()` returning `null` / `""` (round 2). - [x] `connector-framework` test suite after round 2 — 194 tests, 0 failures. - [x] `connector-framework`, `connector-framework-contract`, `OpenICF-ldap-connector` build after round 2. - [x] `mvn install` on `connector-framework`, `connector-framework-contract`, `OpenICF-ldap-connector` (incl. existing test suites, LDAP/OpenDJ integration tests included) — all green, 946 tests, 0 failures (before the review rounds).
… NumberFormatException (#139) Closes 48 of the 54 remaining `java/uncaught-number-format-exception` alerts (the 8 in `AttributeTypeUtil` were already fixed in #134). Dismisses the remaining 6 as unreachable in practice, with the reasoning recorded on each alert. ## AD/LDAP cluster — 26 of 28 sites `ADUserAccountControl`, `ADGroupType` and `ActiveDirectoryChangeLogSyncStrategy` share one root cause: Active Directory bitmask attributes (`userAccountControl`, `groupType`) and sync tokens are parsed with plain `Integer.parseInt`, nothing around it. `ADLdapUtil` gets `parseADInteger`/`parseADLong` (new, unit-tested), and all three classes route through it — a corrupted AD attribute, or a sync token tampered with by whoever calls the connector's `sync()` operation, now fails with a `ConnectorException` naming the value instead of a bare `NumberFormatException` three frames deep with no context. The sync strategy's USNs (`uSNChanged`, `highestCommittedUSN`) are 64-bit in AD, so they go through `parseADLong` and the changes are ordered in a `TreeMap<Long, SyncDelta>`; before, a domain controller past `Integer.MAX_VALUE` could not sync at all. `PagedSearchStrategy`'s paged-results cookie is `<base64 cookie>:<context index>`; the base64 half was already inside a `try`/`catch(RuntimeException)` reporting `ConnectorException`, the numeric half was one line below it, outside. Moved in, and an index outside `baseDNs` (`AAAA:5`, `AAAA:-1`, and `AAAA:1` against a single base DN) is now rejected the same way instead of escaping as an `IndexOutOfBoundsException`. Left alone and dismissed as unreachable (`ADLdapUtil.binarySIDtoString`, 2 sites): the hex strings it parses are built two lines above via `String.format("%02X...")` from raw bytes, so they are always valid hex. ## Elsewhere — 22 sites, same idiom as what already surrounds them - `PropertyBag.castValue` (maven-plugin, 8): POM `<configuration>` values converted to typed connector config properties at build time; the conversion chain is wrapped as a whole, reporting through the `MojoExecutionException` the method already uses two lines above for the equivalent blank-value case. - `SQLUtil.attribute2jdbcValue` (dbcommon, 6): converts an attribute value to a JDBC-bindable value for a numeric SQL column; wrapped with `ConnectorException`, already imported and used elsewhere in the file. - `XmlObjectDecoder` (connector-framework-internal, 5): the five primitive decoders of the XML wire protocol — the one genuinely network-facing trust boundary in this batch. Wrapped with `ConnectorException`, matching `decodeClass`'s existing convention two methods above; an empty element (no text node, which `#PCDATA` allows) is rejected the same way instead of an NPE. - `RemoteWSFrameworkConnectionInfo.loadSystemProxy` (1): a typo in `-Dhttp.proxyPort` now fails clearly at startup instead of with a bare NFE. - `ConnectorHelper` / `AuthenticationApiOpTests` (connector-framework-contract, 2): contract-test configuration (`-DserverPort`, a test-suite attribute) now reports through `ContractException`, the framework's own convention, instead of a bare NFE. ## Dismissed as unreachable — 6 - `ADLdapUtil.binarySIDtoString` ×2 (see above). - `TstStatefulConnectorConfig` (testbundlev1): parses a revision the same test connector generated two lines above via `AtomicInteger.getAndIncrement()`/`String.valueOf()` — no external input. - `JavaScriptExecutorFactory`: parses the JVM's own `java.specification.version`, guaranteed valid by the JDK. - `CSVFileConnector` ×2: parses a substring the regex `\.[0-9]{13}$` just matched immediately before — always exactly 13 ASCII digits. ## Tests New: `ADLdapUtilTest` (8), `ADUserAccountControlTest` (2), `ADGroupTypeTest` (2), `PagedSearchStrategyTest` (4), `SQLUtilTests` (+3), `XmlObjectDecoderTest` (11, corrupts or empties a value inside a real serialized document rather than hand-building the XML schema), `RemoteWSFrameworkConnectionInfoTest` (1), `PropertyBagTests` (2, through the public `mergeConfigurationProperties`) — 33 new/changed tests. Each is RED before its fix and green after, except four that are green on master by design: `decodesAValidInt`, `isAccountDisabledReadsTheBitmask` and `isScopeGlobalReadsTheBitmask` are positive controls for the parse path, and `convertsAnIntWithoutTheUnsupportedWarning` guards against the round-1 regression where a split `if` chain logged "Cast to targetType ... is not supported" for every numeric property. The wrapping tests (`ADLdapUtilTest`, `XmlObjectDecoderTest`, `SQLUtilTests`, `RemoteWSFrameworkConnectionInfoTest`, `PropertyBagTests`) pin the exact message and the `NumberFormatException` cause, so a catch that drops either fails. `ConnectorHelper`/`AuthenticationApiOpTests` are contract-test configuration parsers with no dedicated unit tests of their own; verified by compiling and by the module's existing suite. Local runs after rebasing onto current master (`1abfe741`, through #143), one reactor build of every touched module with its dependencies, all green: connector-framework 197, dbcommon 89, connector-framework-internal 520 (2 skipped, same as on master), connector-framework-contract 43, maven-plugin 3, connector-framework-server 33, ldap-connector 179 (embedded OpenDJ). The counts are higher than in earlier rounds because master gained tests meanwhile (e.g. `ADUserAccountControlTests` from #141).
…nterned-string locks, racy lazy init (#132) Closes the remaining 10 open CodeQL alerts of severity `error` that 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 | Where | Finding | Change | |---|---|---| | `SQLUtil.setParam` | `else if (val instanceof Integer)` appears twice; the second branch can never run | second branch removed | | `ContractITCase.createInstances` | `IObjectFactory objectFactory = null` is dereferenced for any test class with a `(String)` constructor. None of the default classes has one today, so the code only worked because the `NoSuchMethodException` path was always taken | the `(String)` constructor is called through plain reflection, so the method no longer depends on TestNG's internal `ObjectFactoryImpl`, which TestNG 7.5 moved and whose `IObjectFactory` 7.10.2 removed. The unused nested `ContractTestFactory` with its three never-read fields is gone | | `BatchRemoteCache` (testbundlev1) | `synchronized (resultLock)` on the interned literal `"resultLock"`, a monitor shared with any other code in the JVM that synchronises on the same string | `new Object()` | | `WebSocketConnectionGroup.operationContext`, `TstStatefulConnectorConfig.executorService` | double-checked lazy initialisation reads a non-volatile field outside the lock, so a thread may see a partially constructed object | fields declared `volatile`, which makes the pattern correct under the JMM | | `ScriptedConfiguration.getGroovyScriptEngine()` | #133 made the getter `synchronized` for the whole initialisation. That closed the double-checked-locking alert and #149 (the getter and `release()` 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) | on top of #133's synchronised getter, the engine is published only after `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 with `InvokerHelper.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 on `initializeCustomizer()` says so | | `customize` in `SSHConfiguration`, `ScriptedCRESTConfiguration` | with the retry above, a failed attempt that registered `release {}` left its closure for `release()` to run after a successful retry that registered none: the SSH `customize` never reset it, and CREST's `release = null` wrote a private field nothing reads | both reset it with `setReleaseClosure(null)`, as REST's `customize` already does; CREST's unread field is removed | | `FrameworkUtil.getFrameworkVersion()` | unsynchronised lazy init while `setFrameworkVersion` is synchronised | `static synchronized` (not a hot path). The `(ClassLoader)` overload reads `connectors-framework.properties` rather than the field and is only used by `FrameworkUtilTests`, so it is renamed to `readFrameworkVersion` and the getter/setter pairing no longer applies to it | `ScriptedConfigurationTest` covers #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, no `removeClass`, getter not synchronised). `ScriptedConfigurationTest` (CREST) and `SSHConfigurationCustomizerTest` (SSH) cover the release closure: a first attempt registers `release {}` and fails, the retry registers none, and `release()` must not run the stale closure; both fail without the `customize` reset. #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. `createInstances` takes its classes from a fixed list with no `(String)` constructors, so its reflective path cannot be reached from a test. The class of a customizer that **succeeded** is still never unregistered, so `release()` keeps its loader alive; master behaves the same, and that is tracked separately in #154. Local reactor run (rebased on current master, after #131 #133 #134 #135 #136 #139 #141 #142 #143 #145) of all touched modules and everything they depend on: connector-framework 200, dbcommon 89, framework-internal 524 (2 skipped), contract 43, connector-framework-server 33, groovy-connector 130 (42 skipped as on master, 4 new), ssh-connector 97 (82 skipped, 1 new), testbundlev1 compiles — all green.
Summary
Investigated all 49 open
java/unused-parameterCodeQL alerts. 40 were dismissed on GitHub as false positive/won't-fix (interface/abstract method declarations, uniform dispatch signatures, TestNG DataProvider/Factory injection, Procrun stop(String[] args) convention, and public extensibility hooks). The remaining 9 pointed at real problems, fixed here:ADUserAccountControl: the private constructor assigneduacto both theuacandmsDSUacfields instead of keeping them separate, soisAccountLockOut()/isPasswordExpired()never read the real value of AD'sms-DS-User-Account-Control-Computedattribute.JavaScriptExecutorFactory: accepted aClassLoaderbut never applied it (unlike its Groovy sibling, which passes it intoGroovyShell). JS scripts always ran under the ambient thread context classloader instead of the one the caller requested. Fixed by scoping the thread context classloader aroundeval(). Anullloader leaves the caller's thread context classloader untouched, asGroovyShellfalls back to its own loader for a null parent.OpenICFWebSocketCreator.unauthorized(): dropped the specificmessageit was passed and always sent the same generic rejection reason.Also removes genuinely dead
OperationOptions/typeNameparameters from private helpers and updates their call sites:ActiveDirectoryChangeLogSyncStrategy.handleEvents,SchemaApiOpTests.getTestPropertyOrFail,CSVFileConnector.findAccount/doDelete/doUpdate.Test plan
ADUserAccountControlTests,JavaScriptExecutorFactoryTests,UnauthorizedResponseTest— RED before the fix, GREEN after.mvn installonconnector-framework-internal,connector-framework-contract,connector-server-jetty,OpenICF-ldap-connector,OpenICF-csvfile-connector(incl. their existing test suites) — all green.null-loader case, and thatunauthorized()forwards its reason with a 403. Each kills a mutant the earlier tests let through (missingfinallyrestore, set-eval-restore without try/finally, no null guard, hard-coded reason, non-403 status). After the rebase onto currentmaster,mvn installonconnector-framework-internal(509 tests) andconnector-server-jetty(36 tests) is green.