Uh oh!
There was an error while loading. Please reload this page.
Fix type-mismatch, index-out-of-bounds and boxed-equality CodeQL alerts - #782
Conversation
BCrypt.char64() range-checked the character with "x > index_64.length"
instead of ">=", so U+0080 passed the guard and indexed one past the end
of the 128-entry table. The salt region of a stored hash is passed to
decode_base64() verbatim by hashpw(), with no character validation, so a
malformed {BCRYPT} userPassword value made a bind throw
ArrayIndexOutOfBoundsException instead of failing with the intended
"Bad salt length" error.
EntryCacheConfigManager keys cacheNameToLevelMap by DN, but three call
sites looked it up with configuration.dn().toNormalizedByteString().
containsKey() was therefore always false, so the check rejecting two
entry caches at the same cache level never fired, and remove() never
removed anything, leaving a stale level behind after a cache was deleted.
GenerateMessageFileMojo compared the boxed Integer ordinals of two
MessagePropertyKey objects with ==. Since these keys are TreeMap keys,
two messages sharing an ordinal above the Integer cache range compared
as equal and one silently replaced the other, defeating the duplicate
ordinal check that is supposed to fail the build. The same method also
returned 0 for a key with an ordinal versus one without, which dropped
entries as well; compareTo is now a total order with Objects.equals and
keys without an ordinal sorting first.
maximthomas
left a comment
There was a problem hiding this comment.
All three defects are real, and the BCrypt and EntryCacheConfigManager fixes are correct as-is. The comparator fix in GenerateMessageFileMojo is also correct in isolation, but the mojo's downstream code depended on the broken ordering — as written it breaks the doc build in two ways.
I simulated both comparators against the real message files (opendj-server-legacy/src/messages/org/opends/messages/*.properties, copied unfiltered into opendj.jar per opendj-server-legacy/pom.xml:516) over the 19 categories configured in opendj-doc-generated-ref/pom.xml:136-154:
| category | master | this PR |
|---|---|---|
tool | 480 messages documented | 1 documented |
protocol | 248 documented | BUILD FAILURE |
| others | unchanged | unchanged |
protocol log reference now fails the build (blocking)
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties has a genuine duplicate ordinal:
protocol.properties:788—ERR_CANNOT_DECODE_CONTROL_VALUE_1508protocol.properties:863—ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1508
On master the buggy comparator collapsed these into one TreeMap entry, so the duplicate check in getCategoryMap() never fired. With the fix both survive and the mojo throws MojoExecutionException: The ordinal value '1508' ... has been previously defined in protocol.
The PR restores the check without fixing the data it now rejects. ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST has zero Java references and exists only in the base properties file (no locale variants), and 1538 is the highest ordinal there — so renumbering it to 1539 (or deleting it) is a one-line fix.
tool category collapses from 480 documented messages to 1 (blocking)
MessageRefEntry.compareTo() in opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java:180-186 has the same return 0 defect the PR just fixed in MessagePropertyKey, and is left untouched:
if (this.ordinal != null && mre.ordinal != null) {
returnthis.ordinal.compareTo(mre.ordinal);
}
return0; // any null-ordinal entry compares "equal" to everythinggetCategoryMap() feeds keys into TreeSet<MessageRefEntry> in errorMessages.keySet() order. The new comparator sorts null-ordinal keys first, so the first entry inserted becomes the root with a null ordinal, and every subsequent add() compares 0 against it and is discarded as a duplicate. tool.properties has 19 ERR_ keys with no ordinal (ERR_LDAP_CONN_BAD_HOST_NAME, ERR_FAILED_TO_CONNECT, ...); on master they were swallowed inside the TreeMap, so the TreeSet root had a real ordinal and 480 entries survived.
Fix MessageRefEntry.compareTo() in the same PR — the class already has a unique xmlId to break ties, and MESSAGE_NO_ORDINAL (used in toMap(), line 167) shows null-ordinal entries are meant to be documented:
@OverridepublicintcompareTo(MessageRefEntrymre) {
if (!Objects.equals(ordinal, mre.ordinal)) {
if (ordinal == null) {
return -1;
}
if (mre.ordinal == null) {
return1;
}
returnordinal.compareTo(mre.ordinal);
}
returnxmlId.compareTo(mre.xmlId);
}That turns the regression into a real improvement — it also fixes the pre-existing collapses in admin_tool and quickSetup:
| category | master | PR as-is | PR + this fix |
|---|---|---|---|
tool | 480 | 1 | 499 |
admin_tool | 1 | 1 | 260 |
quickSetup | 1 | 1 | 14 |
(Sorting null ordinals last in MessagePropertyKey instead would restore tool to 480 but leaves the other two broken — I'd take the fuller fix.)
Stated testing does not cover the mojo change (blocking)
mvn -pl opendj-doc-maven-plugin,opendj-server-legacy compile never runs generate-xml-messages-doc. That goal is bound to prepare-package in opendj-doc-generated-ref (opendj-doc-generated-ref/pom.xml:124-128), a module built only under the packages profile on Linux (opendj-packages/pom.xml:54). CI runs mvn verify -P precommit (.github/workflows/build.yml:97), which never builds it — so neither CI nor the stated testing would catch either issue above; they surface first in an RPM/DEB/release build.
Please validate with:
mvn -Ppackages -pl opendj-doc-generated-ref -am packageand diff the generated target/asciidoc/source/partials/log-message-reference.adoc.
Missing regression test for BCrypt.char64() (minor)
The fix is correct and the reachability analysis holds: BcryptPasswordStorageScheme.passwordMatches() (opendj-server-legacy/src/main/java/org/opends/server/extensions/BcryptPasswordStorageScheme.java:118) catches only IllegalArgumentException, so pre-fix the ArrayIndexOutOfBoundsException escaped into bind processing; post-fix crypt_raw()'s length check (BCrypt.java:635) throws the catchable IllegalArgumentException.
opendj-server-legacy/src/test/java/org/opends/server/extensions/BCryptTest.java already exists — nothing currently stops >= from regressing to >:
@Test(expectedExceptions = IllegalArgumentException.class)
publicvoidcheckPwRejectsNonAsciiSalt() {
BCrypt.checkpw("secret", "$2a$10$�aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}Nits
EntryCacheConfigManagerbehaviour change:isConfigurationChangeAcceptable()previously always returned true for the level-collision case; it now rejects a change that moves a cache onto an occupied level. Correct, but admin-visible on upgrade — worth a release note. (DN.equals()/hashCode()both delegate totoNormalizedByteString()atopendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java:450-467, so the newDNkeys are semantically identical and cheaper —hashCodeis memoised.)- Dead check in
char64():x < 0is unreachable sincecharis unsigned;if (x >= index_64.length)alone would be clearer while you're on the line.
The restored duplicate ordinal check immediately failed the doc build: protocol.properties has defined ordinal 1508 twice since 2016, and the collapsing comparator was exactly what hid it. The corruption is visible in the checked-in log reference, which documents ID 1508 under the name ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1508 but with the message text of ERR_CANNOT_DECODE_CONTROL_VALUE. ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST has no Java reference and no locale variants, so it is renumbered to 1539, and 1508 goes back to ERR_CANNOT_DECODE_CONTROL_VALUE, which is used by ExternalChangelogRequestControl and exists in all six translations. MessageRefEntry.compareTo() had the same defect as MessagePropertyKey: it returned 0 whenever either ordinal was null, so a null ordinal entry compared equal to everything in the TreeSet holding it. With null ordinals now sorting first, the first entry inserted became the root of the tree and every subsequent add() was discarded as a duplicate, which would have collapsed the tool category from 480 documented messages to 1. It is now a total order too, with the already unique xmlId breaking ties, which also repairs the pre-existing collapses in admin_tool (1 -> 260) and quickSetup (1 -> 14). A BCryptTest case now pins the char64() range check, using U+0080, the first character past the end of the 128 entry decoding table. The unreachable x < 0 half of that check is dropped: char is unsigned.
vharseko
commented
Jul 29, 2026
Thanks — both blocking findings reproduce, fixed in 9e1ee83. I re-ran the check independently, driving the compiled
1. Ordinal 1508Renumbered The damage is visible in the checked-in reference, 2. |
| input | master | this branch |
|---|---|---|
"$2a$10$^@aaaa…" | IllegalArgumentException: Bad salt length | IllegalArgumentException: Bad salt length |
"$2a$10$" + (char) 0x80 + "aaa…" | ArrayIndexOutOfBoundsException: Index 128 out of bounds for length 128 | IllegalArgumentException: Bad salt length |
The test uses U+0080, the first character past the end of the 128-entry table.mvn -pl opendj-server-legacy verify -P precommit -Dit.test=BCryptTest → Tests run: 77, Failures: 0, Errors: 0. (Worth noting for anyone else validating: mvn test runs nothing in that
module — surefire is bound to phase none at opendj-server-legacy/pom.xml:641-651 and everything
runs under failsafe in the precommit profile.)
5. Nits
x < 0 dropped from char64(). To make sure that stayed behaviour-preserving I compared the
method against both baselines by reflection over the whole char range: 0 differing code units vs
the previous commit of this branch, and exactly one vs master — U+0080, whereArrayIndexOutOfBoundsException becomes -1. The decoding table is untouched.
Agreed on the EntryCacheConfigManager release note; the level-collision check now actually
rejects, which is admin-visible on upgrade.
One more, found while checking those numbers
admin_tool reaches 260 rather than its 316 ERR_ keys because parseString() truncates the
description at the last _ even when there is no ordinal:
finalStringdescription = key.substring(0, li).toUpperCase(); // ERR_CTRL_PANEL_INVALID_DAY -> ERR_CTRL_PANEL_INVALIDERR_CTRL_PANEL_INVALID_{DAY,ENTRY,HOUR,MINUTE,TIME} all collapse onto ERR_CTRL_PANEL_INVALID,
so four of the five vanish and the survivor is documented under a truncated name. admin_tool
loses 56 of 316 that way and quickSetup 3 of 17 — same class of defect, present on master too,
and a one-line fix (keep the whole key when there is no ordinal). Fold it into this PR, or open a
separate issue?
Uh oh!
There was an error while loading. Please reload this page.
Fixes five CodeQL alerts that are genuine defects:
java/index-out-of-bounds(#700),java/type-mismatch-access(#684, #685),java/type-mismatch-modification(#696) andjava/reference-equality-of-boxed-types(#559).1.
BCrypt.char64()indexes one past the end of the tableindex_64has exactly 128 entries, soU+0080passes the guard andindex_64[128]throws.This is reachable from the public API.
BcryptPasswordStorageScheme.passwordMatches()callsBCrypt.checkpw(plaintext, storedPassword), which callshashpw(), where the salt region is taken from the stored value with no character validation at all:A malformed or hostile
{BCRYPT}userPasswordvalue therefore makes a bind throwArrayIndexOutOfBoundsExceptioninstead of failing cleanly.Verified against a real build, calling the public API with a stored hash whose salt contains
U+0080:BCrypt.checkpw("secret", "$2a$10$" + U+0080 + ...)masterjava.lang.ArrayIndexOutOfBoundsException: Index 128 out of bounds for length 128java.lang.IllegalArgumentException: Bad salt length— the intended error pathchar64was also exercised by reflection over the whole0..0x10FFrange: it now returns-1for every value above 127 and never throws, while the decoding table itself is unchanged (char64('A') == 2).2.
EntryCacheConfigManagerlooks up aMap<DN, Integer>with aByteStringThe map is declared as
Map<DN, Integer> cacheNameToLevelMapand populated withconfiguration.dn()inloadAndInstallEntryCache().applyConfigurationChange()reads it correctly with aDN, but three call sites usedconfiguration.dn().toNormalizedByteString():isConfigurationChangeAcceptable()—containsKey()always returned false, so the check that rejects a second entry cache at an already used cache level never fired and the conflicting configuration was accepted silently.applyConfigurationDelete()—remove()never removed anything, so the entry stayed in the map after the cache was deleted and a later cache created under the same DN saw a stale cache level.Both now use the
DNdirectly. TheByteStringimport is no longer needed.3.
GenerateMessageFileMojocompares boxedIntegerordinals with==MessagePropertyKeyinstances are used as keys of aTreeMap, socompareTo()defines key identity. Two problems:ordinal == k.ordinalis a reference comparison. For ordinals above theIntegercache range, two keys with the same ordinal fell through toordinal.compareTo(k.ordinal), which returns0, so theTreeMaptreated them as one key and the secondput()overwrote the first. That also defeats the explicit duplicate-ordinal check ingetCategoryMap(), which is supposed to fail the build with "The ordinal value X has been previously defined" — instead a message silently disappeared from the generated documentation.return 0made a key with an ordinal compare equal to a key without one, dropping entries the same way.compareTo()is now a total order:Objects.equals()for the ordinals, keys without an ordinal sort first, ties broken by description.4.
protocol.propertiesdefines ordinal 1508 twiceThe duplicate-ordinal check restored by (3) immediately fails the build, because the data it
guards has been broken since 2016:
protocol.properties:788—ERR_CANNOT_DECODE_CONTROL_VALUE_1508protocol.properties:863—ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1508(added in OPENDJ-3031)The collapsing
compareTo()hid it: the two keys landed in oneTreeMapentry, sogetCategoryMap()only ever saw one ordinal 1508. The corruption is visible in the checked-inopendj-doc-generated-ref/src/main/asciidoc/reference/appendix-log-messages.adoc:11914, whichdocuments ID 1508 under the name
ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1508but with themessage text of
ERR_CANNOT_DECODE_CONTROL_VALUE, while the latter appears nowhere in thereference.
ERR_HTTP_ERROR_WHILE_PROCESSING_REQUESThas no Java reference and no locale variants, and 1538is the highest ordinal in the file, so it is renumbered to 1539; 1508 goes back to
ERR_CANNOT_DECODE_CONTROL_VALUE, which is used byExternalChangelogRequestControland existsin all six translations. It is the only duplicate
ERR_ordinal in the repository.5.
MessageRefEntry.compareTo()has the same defectMessageRefEntryis held in aTreeSet, and itscompareTo()returned0whenever eitherordinal was
null, so a null-ordinal entry compared equal to everything. With (3) sortingnull-ordinal keys first, the first entry inserted becomes the root of the tree and every
subsequent
add()is discarded as a duplicate —toolwould drop from 480 documented messagesto 1. Fixed the same way, with the already unique
xmlIdbreaking ties.Testing
mvn -pl opendj-doc-maven-plugin compile, andmvn -pl opendj-server-legacy verify -P precommit -Dit.test=BCryptTest—Tests run: 77, Failures: 0, Errors: 0. (Note thatmvn testruns nothing inopendj-server-legacy: surefire isbound to phase
noneatopendj-server-legacy/pom.xml:641-651and the tests run under failsafe inthe
precommitprofile.)The log reference generation was driven over the real message files for all 19 categories
configured in
opendj-doc-generated-ref/pom.xml, using the compiledMessagePropertyKeyandMessageRefEntryclasses:protocoltooladmin_toolquickSetupBCrypt.char64()was compared againstmasterby reflection over the wholecharrange: exactlyone code unit behaves differently — U+0080,
ArrayIndexOutOfBoundsExceptionbecomes-1— and thedecoding table itself is unchanged (
char64('A') == 2). The newBCryptTestcase fails onmasterwithArrayIndexOutOfBoundsException: Index 128 out of bounds for length 128and passeshere with the intended
IllegalArgumentException: Bad salt length.Note that
opendj-doc-generated-refis built by the ordinary CI job on Linux —opendj-packagesis in the default module list (
pom.xml:298) and itsdistribution-unixprofile adds the docmodule — which is where the ordinal 1508 failure showed up.