Fix CodeQL warning-severity alerts: dead checks, boxed locals and three null dereferences - #793
Conversation
…r null dereferences Tier 4 of the warning-severity CodeQL triage, covering the remaining sixteen rules. Of the 121 alerts, 64 are fixed here; the other 57 are left as they are for the reasons given at the end. Four of these turned out to be real defects rather than noise: * ModifyDNOperationBasis.getNewDN() checked for a null parent DN, set an UNWILLING_TO_PERFORM result, and then fell through to parentDN.child(), which throws NullPointerException. It now returns null once the error is recorded. The isRootDN() path is unchanged, since a child DN can still be built there. * RootContainer dereferenced highestID, which stays null when there is no base DN to open. Entry numbering now starts at 1 in that case. * ImportTask dereferenced the backend field in two places. With neither a backend ID nor an include branch resolving to a backend it stayed null, so a malformed import task failed with a NullPointerException instead of a diagnosable error. * BuildVersion.compareTo compared the rev field with ==, but rev is a String, so the comparison was by reference. AttributeFilter kept its iteration state in the Iterable rather than in the iterator, so a second traversal, including the one performed by toString(), yielded nothing. The state now belongs to the iterator. Dead checks removed (19 alerts). Each was verified in place and every removal preserves behaviour: a lower bound already implied by the enclosing test in ASCIICharProp and StringPrepProfile; a NUL comparison in the else branch of a c < ' ' test in AVA; a negative test on a value masked with 0x7F in PasswordPolicyResponseControl; an index test already implied by the loop bound in InetAddressValidator; three unreachable checks in SchemaUtils, where length is already 1 and the first character was validated by the enclosing isAlpha test; a length check already performed at the top of the method in UTCTimeSyntaxImpl; a negative array length in TemplateTag; a redundant second comparison in StaticUtils; and six null checks on fields initialised to a new collection. Mechanical fixes (41 alerts): fifteen boxed locals replaced by primitives; eight getClass().getResource calls replaced by the declaring class literal; seven int multiplications widened to long; three non-short-circuit operators rewritten into explicit locals, with a comment on why both sides must be evaluated; the two getFormatter calls in JDKLogging qualified with super, since they resolve to Handler rather than to the enclosing JDKLogging.getFormatter(); two per-call SecureRandom instances replaced by a shared one; an explicit interrupt check in the StatusCli refresh loop; and a dead assignment in replace.rb. Left as they are: * java/dereferenced-value-may-be-null, 37 alerts. All 42 were reviewed individually rather than dismissed as a group: four were real and are fixed above, one is already fixed by the Tier 1 change to AsynchronousTextWriter, and the rest are inferred from a guard on a different path. Resolving them would mean either adding redundant null checks or removing guards that may be needed. * java/inconsistent-compareto-and-equals, 12 alerts. Adding equals and hashCode would change collection semantics for classes that currently rely on identity in HashMap and HashSet. Task was checked as a representative: its compareTo falls back to comparing task IDs, so compareTo returns 0 only for the same task and its TreeSet usage is already correct. * java/constant-comparison, 6 alerts. Two are in DoubleMetaphone, a line-by-line port of the Double Metaphone algorithm where simplifying the conditions risks changing phonetic matching results. Four are retry loop conditions that are trivially true on their first evaluation. * java/iterator-remove-failure, 1 alert. The delegating remove() correctly propagates UnsupportedOperationException.
| else if (attr.isValueInBytes()) | ||
| { | ||
| Long l = Long.parseLong(monitoringValue); | ||
| long l = Long.parseLong(monitoringValue); |
| } | ||
|
|
||
| Double fractionValue = Double.parseDouble(fractionBuffer.toString()); | ||
| double fractionValue = Double.parseDouble(fractionBuffer.toString()); |
maximthomas
left a comment
There was a problem hiding this comment.
Solid triage — the per-alert justification made this reviewable, and I independently re-derived all 19 dead-check removals: every one is genuinely unreachable, and none widens what the parsers accept. Verified on the PR head: all 9 touched modules compile, opendj-core passes 8171/8171 tests (the only failure was opendj-dsml-servlet's cargo container failing to bind port 8080 locally — unrelated).
Four things to address, mostly around the classification of the fixes rather than their content.
StatusCli interrupt guard can never fire (medium)
opendj-server-legacy/src/main/java/org/opends/server/tools/status/StatusCli.java
StaticUtils.sleep (opendj-server-legacy/src/main/java/org/opends/server/util/StaticUtils.java:2357) swallows InterruptedException without restoring the flag, and Thread.sleep clears it on throw. So after an interrupt isInterrupted() is false and the loop continues exactly as before — I confirmed this with a probe replicating both bodies: 21 iterations post-interrupt(), isInterrupted() == false.
The alert is masked, not fixed, and the new comment describes behaviour the code doesn't have. Either make it real:
if (timeToSleep > 0)
{
try { Thread.sleep(timeToSleep); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}…or revert it and dismiss the alert alongside the other 57 — entirely consistent with how the rest of this PR handles cosmetic loop conditions.
ImportTask:331 guard is unreachable (low)
opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
initializeTask() already enforces the precondition at line 215:
// Make sure that either the "includeBranchStrings" argument or the "backendID" argument was provided.
if (includeBranchStrings.isEmpty() && backendID == null) { throw … ERR_LDIFIMPORT_MISSING_BACKEND_ARGUMENT … }So at line 328 either backendID != null (and the getLocalBackendById null case already threw), or includeBranches is non-empty and the loop's else throws ERR_NO_BACKENDS_FOR_BASE for every unresolved branch. backend cannot be null.
The runTask guard (:479) is live — runTask never re-validates and silently ignores unresolvable branches (no else), so it's reachable when a backend disappears between scheduling and execution. Keep that one; drop :331, or keep it and label it defensive. As written it's the redundant null check the PR explicitly declines to add for the 37 dereferenced-value-may-be-null alerts.
Defect classification is over-claimed (low)
Of the five highlighted "real defects":
| Site | Actually |
|---|---|
BuildVersion.compareTo():194 |
Real, reachable |
ImportTask:479 |
Real, reachable |
ModifyDNOperationBasis.getNewDN():530 |
Real NPE (two null paths: getEntryDN() returns null on a malformed raw DN, and getParentDNInSuffix returns null for a suffix root) — but no in-tree production caller |
ImportTask:331 |
Unreachable (above) |
RootContainer:232 |
Unreachable — base-dn is mandatory="true" in opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/BackendConfiguration.xml:95, and the sole caller passes config.getBaseDN() |
Meanwhile the one clearly user-triggerable defect is filed under "mechanical": purgeTaskMaxDurationInSec in opendj-server-legacy/src/main/java/org/opends/server/tasks/PurgeConflictsHistoricalTask.java is Integer.decoded from a task attribute with no upper bound, so e.g. 3000000 overflowed * 1000 to a deadline ~15 days in the past. That belongs in the defects table; the other six widened multiplications are unreachable with realistic inputs.
Also worth softening the AttributeFilter claim: all four in-tree filteredViewOf sinks traverse exactly once and nothing holds an Iterable<Attribute>, so this is a latent Iterable-contract violation on a public API, not an active failure. toString() alone was never broken (each getAllAttributes() call returns a fresh Iterable). The fix is still right.
getNewDN() null result isn't memoised (low)
opendj-server-legacy/src/main/java/org/opends/server/core/ModifyDNOperationBasis.java
null isn't cached in newDN, so a second call repeats the lookup and appends ERR_MODDN_NO_PARENT again — appendErrorMessage concatenates with a " " separator (opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java:285-287). Low impact (no in-tree caller), but a newDNComputed flag avoids it.
Related: on the malformed-DN path valueOfRawDN has already set INVALID_DN_SYNTAX, which this block then overwrites with UNWILLING_TO_PERFORM — so "the error set above is the outcome" masks the better diagnosis.
Nits
- Duplicate copyright lines:
opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.javaalready carriesPortions copyright 2021-2026 3A Systems, LLC— the added 2026 line is redundant, and it's inserted above the ForgeRock line, breaking chronological order.opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaUtils.java(2024) andopendj-server-legacy/src/main/java/org/opends/server/types/Entry.java(2023-2024) should extend their ranges rather than gain a second line. The other 42 files are consistent. BuildVersion.compareTocomputes the comparison twice:rev.equals(…)thenrev.compareTo(…). The whole nested block collapses toreturn Integer.signum(rev.compareTo(version.rev));. (Confirmed the siblingorg.opends.server.util.BuildVersiondoesn't comparerevat all — no second instance.)JDKLoggingsuper.is cosmetic: verified by probe that the unqualified call already bound to inheritedHandler.getFormatter(), never the outer static —OpenDJHandlerisprivate static final, sosuper.≡this..super.setFormatter(…)was never ambiguous. Renaming the outer static would remove the collision at its source.- Orphaned comment: in
runTaskthe new guard was inserted after// Find backends with subordinate base DNs…, separating it from the code it describes — move the guard above it. AlsoERR_LDIFIMPORT_NO_BACKENDS_FOR_ID's text fits:479but reads oddly for a "neither was specified" case; a comment saying you're reusing it to avoid new message ordinals would help. Rest2LdapHttpApplicationis the oneunsafe-get-resourcechange with real semantics: the class is explicitly extensible (public,protected final configDirectory/schema, "Default constructor called by the HTTP Framework").getClass().getResourceresolves against the runtime subclass's loader; the class literal againstopendj-rest2ldap's. Same webapp loader → identical; a subclass in a separate WAR/bundle shipping its ownconfig.json→ breaks. The other seven are safe (ProductInformationisfinal, Mojos andCommonAuditinternal).- Test coverage: nothing added for six behaviour-affecting fixes.
AttributeFilterhas no test referencing it at all, yet this PR changes its observable contract — iterate a filtered view twice and assert both traversals match.BuildVersion.compareToon an equal-but-distinctrevreturned1before and0now: two lines.readOIDLenalso has no test (the removal rests purely onint length = 1, which does hold). - Merge order:
Utilities.javawill definitely conflict, not just likely — #789 edits base lines 2413-2419, this PR 2411-2417, overlapping in the sameisNumericDatebranch, plus both add a 3A header line at the same position. Suggested order #789 → #790 → #791 → this one is sound. No semantic overlap on theSecureRandomwork (#789 doesSha2Crypt, this doesSaltedSHA1/SaltedSHA512) — same pattern, different files. - Startup cost: the new
static final SecureRandomfields seed at class-init in classes the server loads at startup; on entropy-starved hosts with a blocking implementation that's a small delay. A lazy holder would avoid it. Not a blocker.
StatusCli now sleeps with Thread.sleep and stops on interruption: StaticUtils.sleep discards the InterruptedException without restoring the flag, so the interrupt check added to the loop condition could never become true. ImportTask drops the null check in initializeTask(), which cannot be reached because that method already rejects a task carrying neither a backend ID nor an include branch. The check in runTask() stays, since that method does not re-validate and ignores an include branch resolving to no backend. ModifyDNOperationBasis remembers that the new DN has been computed, so a failed computation is neither repeated nor its error message appended twice, and it leaves the INVALID_DN_SYNTAX diagnosis of an unparseable entry DN in place. BuildVersion compares the revisions once instead of testing for equality first. Adds the tests the behaviour changes lacked: a filtered view can be iterated more than once, and versions differing only by an equal-valued but distinct revision string compare equal. Merges the duplicated 3A Systems copyright lines.
|
Thanks — all four are addressed in 25625bc, along with the nits.
|
| return NO_VALUE_SET.toString(); | ||
| } | ||
| Long l = Long.parseLong(monitoringValue); | ||
| long l = Long.parseLong(monitoringValue); |
Tier 4 of the warning-severity CodeQL triage, covering the remaining sixteen rules (Tier 1 is #789, Tier 2 is #790, Tier 3 is #791). Of the 121 alerts, 63 are fixed here; the other 58 are left as they are, with the reason for each given below.
Real defects, and how reachable each one is
PurgeConflictsHistoricalTask.runTask()purgeTaskMaxDurationInSecisInteger.decoded from a task attribute with no upper bound, and* 1000overflowedint. A max duration of 30 days produced a deadline about 20 days in the past, sopurgeConflictsHistoricalaborted on the very first entry withADMIN_LIMIT_EXCEEDED— whichrunTask()maps toCOMPLETED_SUCCESSFULLY, leaving only the purge-completed-in-time attribute atfalse. Widened tolong.BuildVersion.compareTo():194rev == version.revcomparedStringreferences —revis aString, as therev.compareTo(...)two lines below shows — so two builds carrying the same revision from different sources never compared equal. The nested block is now a singleInteger.signum(rev.compareTo(...)).ImportTask:479runTask()does not re-validate the task and silently ignores an include branch which resolves to no backend, sobackendcould still be null and was dereferenced immediately. Guarded with the existingERR_LDIFIMPORT_NO_BACKENDS_FOR_ID.ModifyDNOperationBasis.getNewDN():535parentDN == nullbranch set anUNWILLING_TO_PERFORMresult and then fell through toparentDN.child(...), throwingNullPointerException. It now returnsnullonce the error is recorded, remembers that the computation has run so the error is not appended a second time, and leaves theINVALID_DN_SYNTAXdiagnosis of an unparseable entry DN in place rather than overwriting it. TheisRootDN()path is unchanged, since a child DN can still be built there.getNewDN()is reached only from testsRootContainer:232highestID.longValue()was dereferenced althoughhighestIDstays null when there is no base DN to open. Entry numbering now starts at 1 in that case.base-dnismandatory="true"inBackendConfiguration.xmland the sole caller passesconfig.getBaseDN(), so this is hardening rather than a fixAttributeFilter(java/iterable-wraps-iterator) kept its iteration state in theIterablerather than in the iterator, so theIterablecould be traversed only once — a second traversal, including one following its owntoString(), yielded nothing. Every in-tree caller traverses exactly once, which makes this a latent violation of theIterablecontract on a public API rather than an active failure. The state now belongs to the iterator, and a test covers both traversals.The
dereferenced-value-may-be-nullalert onImportTask:331is not fixed:initializeTask()already rejects a task carrying neither a backend ID nor an include branch, and its loop throws for every branch which resolves to no backend, sobackendcannot be null there. It is counted below with the other alerts left in place.Dead checks removed — 19 alerts
Each was verified in place; every removal preserves behaviour.
ASCIICharProp×2c >= 'a'inside a branch guarded byc >= 'a' && c <= 'z'StringPrepProfile(b & 0x7F) != btest has already skipped every negative byteAVAelsebranch of ac < ' 'testPasswordPolicyResponseControl0x7FInetAddressValidatorindex > octets.length - 1under a loop bound ofindex < octets.lengthSchemaUtils×3lengthis already 1, and the first character was validated by the enclosingisAlpha(c)— the check is duplicated, not lostUTCTimeSyntaxImpllength < 11is already rejected at the top of the methodTemplateTagjava/test-for-negative-container-size)StaticUtilsfirstByte != secondByteis redundantuseless-null-check×6NameForm,GenericDialog,BindRule,LDIFBackend,DsMIBImpl×2 — null checks on fields initialised to a new collection, or on a freshly constructed objectMechanical fixes — 39 alerts
java/non-null-boxed-variable).getClass().getResource…calls replaced by the declaring class literal (java/unsafe-get-resource), so the lookup no longer depends on the runtime subclass. The only documented extension point among them isRest2LdapHttpApplication(two of the eight): a subclass loaded by the same web application resolves identically, but one shipping its ownconfig.jsonin a separate WAR or bundle would no longer find it. The remaining six are in afinalclass, two Mojos, a servlet and an internal logger.long(java/integer-multiplication-cast-to-long). One is thePurgeConflictsHistoricalTaskdefect above; of the rest onlyStatusCli's refresh period is also unbounded user input, and the remaining five need values no deployment produces.&&/||, with a comment on why both sides must be evaluated —StaticUtils.recursiveDeleteandCronExecutorService.cancel/awaitTerminationdeliberately used&/|, and this keeps that intent while removing the alert.getFormatter()calls inJDKLoggingqualified withsuper.(java/subtle-inherited-call).OpenDJHandleris a static nested class, so the unqualified calls already bound toHandler's formatter and not to the enclosingJDKLogging.getFormatter(); the qualification only makes that unambiguous.SecureRandominstances in the salted SHA schemes replaced by a shared one (java/random-used-once). The third alert of this rule, inSha2Crypt, is fixed by Fix CodeQL warning-severity alerts: weak salt PRNG, unsafe DCL, thread-unsafe date formats #789.StatusClirefresh loop now sleeps withThread.sleepand stops when it is interrupted (java/constant-loop-condition). Adding an interrupt check to the loop condition alone would not have worked:StaticUtils.sleepdiscards theInterruptedExceptionwithout restoring the flag, so the check could never have fired.replace.rb(rb/useless-assignment-to-local).Left as they are — 58 alerts
java/dereferenced-value-may-be-nullRootContaineras hardening — one more is already fixed by #789 inAsynchronousTextWriter, and the rest — includingImportTask:331— are inferred from a guard on a different path or are already excluded by a precondition. Resolving them would mean either adding redundant null checks (making the code worse) or removing guards that may be needed.java/inconsistent-compareto-and-equalsequals/hashCodewould change collection semantics for classes that currently rely on identity inHashMap/HashSet.Taskwas checked as a representative: itscompareTofalls back to comparing task IDs, so it returns 0 only for the same task and theTreeSet<Task>usage is already correct.java/constant-comparisonDoubleMetaphone, a line-by-line port of the Double Metaphone algorithm where simplifying the conditions risks changing phonetic matching results. Four are retry-loop conditions (for (int i = 0; i < 2; ++i),while (stopTries > 0)) that are trivially true on first evaluation; restructuring a retry loop for a cosmetic alert is not worth it.java/iterator-remove-failureremove()correctly propagatesUnsupportedOperationException.These are better closed as dismissals in Code scanning with a reason than by changing code, and have been dismissed there.
Verification
mvn -o verify(javadoc skipped — the generatedDsconfigMessagesfails the JDK 26 doclint locally, unrelated to this change) foropendj-configandopendj-core: 8173 + 547 tests pass, 0 failures.mvn -o compileacross all touched modules: passes.AttributeFilterTestCasereportsexpected:<['objectClass', 'cn', 'sn']> but was:<[]>,BuildVersionTestreportsexpected [0] but found [1].Note for reviewers
Files also touched by the earlier PRs:
Utilities.javaandCommonAudit.java(#789),ReplicationBroker.java(#790),Installer.javaandBindRule.java(#791).Utilities.javawill conflict — #789 edits base lines 2413-2419 and this PR 2411-2417, inside the sameisNumericDatebranch, and both add a copyright line at the same position. Suggested order: #789 → #790 → #791 → this one.