Resolve open CodeQL alerts in the persistit module#295
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
Re-reviewed after ec09b3d. The Java baseline is fine — the parent pom targets 11 and animal-sniffer was dropped in 0f5e38085, so a Java 9+ API is safe here. But the new commit rests on an unreachability claim that the constants contradict, and that's now the blocker. The other ten files are unchanged from the first round; my earlier points on them stand.
Exchange: the removed guard is reachable, and it was absorbing an off-by-one (moderate)
persistit/core/src/main/java/com/persistit/Exchange.java
final int lvl = level >= 0 ? level : _tree.getDepth() + level;
final LevelCache[] levelCache = _levelCache;
Objects.checkIndex(lvl, levelCache.length);The commit message says "the check stays redundant given the earlier tree-depth guard … only the exception type on the unreachable branch changes." It's redundant only if _tree.getDepth() <= MAX_TREE_DEPTH (20), and the constants don't guarantee that:
Tree.setRootPageAddressderives depth from the root page type, validatingtype <= PAGE_TYPE_INDEX_MAX, then setsversion._depth = type - PAGE_TYPE_DATA + 1. WithPAGE_TYPE_DATA = 1andPAGE_TYPE_INDEX_MAX = 21, that yields depth up to 21 — one more thanMAX_TREE_DEPTH = 20, which is_levelCache.length.Tree.load(Tree.java:316) applies no validation at all:version._depth = Util.getShort(bytes, index + 16);, andUtil.getShortsign-extends, so a damaged directory record yields any value in[-32768, 32767].- Nothing bounds growth either —
Tree.changeRootPageAddrjust doesversion._depth += deltaDepth;, andMAX_TREE_DEPTHappears in no guard anywhere (only array sizings and loop bounds).IntegrityCheck.java:889treats an over-deep tree as a reportable fault (addFault("Tree is too deep", …)), i.e. as possible.
With depth = 21, level = 20 clears the first guard (20 >= 21 is false), lvl = 20, and the second check fires. That's not a hypothetical corner: CLI.java:1044 drives the method straight off the disk value —
final int depth = _currentTree.getDepth();
for (int level = depth; --level >= 0;) {
final Buffer copy = exchange.fetchBufferCopy(level);So the caller used to get IllegalArgumentException("Tree depth is 21") and now gets IndexOutOfBoundsException("Index 20 out of bounds for length 20"). Two consequences: the depth disappears from the message on exactly the path used to diagnose a suspect volume, and because IndexOutOfBoundsException is a sibling of IllegalArgumentException rather than a subclass, any catch (IllegalArgumentException) around this public method stops catching it. ManagementImpl.getBufferInfo (ManagementImpl.java:669) catches only PersistitException, so it propagates over RMI to the AdminUI either way.
Simplest resolution: keep the previous commit's version — the local capture plus the original if. I checked it and it is correct and behaviour-preserving (_levelCache is private final … = new LevelCache[MAX_TREE_DEPTH], Exchange.java:267), so if alert #2090 survives it, it is a false positive and dismissing it is defensible. If you'd rather keep the sanitizer, preserve the contract:
final LevelCache[] levelCache = _levelCache;
try {
Objects.checkIndex(lvl, levelCache.length);
} catch (final IndexOutOfBoundsException e) {
throw new IllegalArgumentException("Tree depth is " + _tree.getDepth(), e);
}Either way, please drop the "unreachable" wording from the commit message.
Missing test for the only behavioural change (medium)
JoinPolicy.forName compared policy.toString() against the name, so it matched nothing and always threw. Configuration.setJoinPolicy(String) — the joinpolicy property — was therefore broken for every value:
persistit/core/src/main/java/com/persistit/Configuration.java
public void setJoinPolicy(final String policyName) {
if (policyName != null) {
setJoinPolicy(JoinPolicy.forName(policyName)); // always threw before this PR
}
}Adding toString() repairs it. Nothing in the repo exercises forName or the joinpolicy property, which is why this went unnoticed. Please lock it in — SplitPolicyTest is the precedent for the location:
persistit/core/src/test/java/com/persistit/JoinPolicyTest.java
@Test
public void forNameResolvesPolicies() {
assertSame(JoinPolicy.EVEN_BIAS, JoinPolicy.forName("even"));
assertSame(JoinPolicy.LEFT_BIAS, JoinPolicy.forName("LEFT"));
assertSame(JoinPolicy.RIGHT_BIAS, JoinPolicy.forName("Right"));
}
@Test(expected = IllegalArgumentException.class)
public void forNameRejectsUnknown() {
JoinPolicy.forName("NOPE");
}BufferPool: message churn inside the OOME handler (minor)
persistit/core/src/main/java/com/persistit/BufferPool.java
final int reserved = reserve.length;
reserve = null;
System.err.print("Out of memory after reserving ");
System.err.print(reserved);
System.err.print(" bytes, with ");reserved is always 1024 * 1024 — a constant, so the new text adds no diagnostic value while adding two more String-allocating print calls to a path whose own comment says it is "written this way to try to avoid another OOME." Reading reserve is what clears the alert; the message change isn't needed.
ThreadSequencer: binary-incompatible for external consumers (minor)
persistit/core/src/main/java/com/persistit/util/ThreadSequencer.java
Safe in-repo — none of the 30 SequencerConstants members are referenced inside ThreadSequencer, there are no wildcard static imports of it, and every constant user imports SequencerConstants directly. But ThreadSequencer is public in a published artifact, so code compiled against ThreadSequencer.COMMIT_FLUSH_A will now fail with NoSuchFieldError. Low risk for an internal test hook; worth a line in the release notes.
Nits
- Exchange — document the thrown exception:
fetchBufferCopyhas no@throwsin its javadoc at all. Whichever exception you settle on, state it — that turns the choice above into a decision rather than an accident. - Exchange — discarded return value:
Objects.checkIndexreturns the index; ignoring it may itself attract ajava/ignored-return-value-style alert. Worth a glance at the next scan so you don't trade one finding for another. - JoinPolicy — wrong type in the error message:
throw new IllegalArgumentException("No such SplitPolicy " + name);— copy-paste fromSplitPolicy, and you're already in the file.persistit/core/src/main/java/com/persistit/policy/JoinPolicy.java - JoinPolicy —
forNamecould usegetName(): comparingpolicy.getName()clears the samejava/call-to-object-tostringalert and can't be broken by a subclass overridingtoString()(the ctor isprotected). MatchingSplitPolicy's existing style is defensible too — your call. - InspectorPanel — the added guard is unreachable:
Fetcheris only constructed atInspectorPanel.java:137, insiderefresh(), which already returns early onlr == null; the field is nulled only later inrun(). Harmless, but a short "defensive, for static analysis" comment stops someone deleting it again.persistit/ui/src/main/java/com/persistit/ui/InspectorPanel.java - ManagementTableModel — say that the parse is load-bearing: keeping the token consumption is required, not just validation.
TaskStatus.column.3 = getState:100:A:State:30:TaskStatusStateRendereris the one spec supplying the optional 5th token; dropping the read would shiftTaskStatusStateRendererinto the min-width slot and break renderer resolution. Min width is really encoded in the width token (_widths[i] / 10000, line 248), so the local was genuinely dead — the change is right, the comment just undersells why theparseIntmust stay.persistit/ui/src/main/java/com/persistit/ui/ManagementTableModel.java
One note on framing: despite the security label, nothing security-relevant changed. The "high" java/improper-validation-of-array-index finding was a false positive — the original bounds check was already correct — so this is a static-analysis legibility fix, not a patched vulnerability. Worth being precise about that in the changelog.
Clears the open code-scanning alerts (1 high, 2 warnings, 8 notes) in the
persistit module. The high java/improper-validation-of-array-index finding
was a false positive - the original bounds check was already correct - so
this is a static-analysis legibility change, not a security fix.
Core:
- Exchange.fetchBufferCopy: replace the manual level bounds check with
Objects.checkIndex (recognized by CodeQL as an array-index sanitizer),
captured against a local reference to the level cache. The bound is
reachable - tree depth can legally reach PAGE_TYPE_INDEX_MAX and is
unbounded on a corrupted volume - so the original
IllegalArgumentException("Tree depth is N") contract is preserved by
rethrowing it from IndexOutOfBoundsException, and the thrown exception
is now documented.
- JoinPolicy: add toString() so forName() matches by policy name instead
of Object identity, repairing the joinpolicy configuration property,
which previously always threw; JoinPolicyTest locks the behaviour in.
Fix the copy-pasted "No such SplitPolicy" message.
- CLI.source: wrap the FileReader directly in the stacked BufferedReader
(java/input-resource-leak).
- BufferPool: read the reserved block before releasing it on
OutOfMemoryError so the reservation stays effective and the variable is
no longer dead (java/local-variable-is-never-read).
- ThreadSequencer: stop implementing the constants-only SequencerConstants
interface (java/constants-only-interface).
- GetVersion: drop a useless toString() call on a String value
(java/useless-tostring-call).
UI:
- InspectorPanel.Fetcher: null-check the logical record before
dereferencing it (java/dereferenced-value-may-be-null).
- ManagementTableModel, ManagementSlidingTableModel: remove locals that
are written but never read, keeping the load-bearing token parse
(java/local-variable-is-never-read).
- AdminUITaskPanel: drop a useless toString() call
(java/useless-tostring-call).
- Remove the unused VTComboBoxModel class (java/unused-reference-type).
ec09b3d to
f18f79e
Compare
|
Thanks for the thorough re-review — you're right on every count. Pushed as a single squashed commit ( Exchange — reachable guard / off-by-oneConfirmed. final LevelCache[] levelCache = _levelCache;
try {
Objects.checkIndex(lvl, levelCache.length);
} catch (final IndexOutOfBoundsException e) {
throw new IllegalArgumentException("Tree depth is " + _tree.getDepth(), e);
}So the depth stays in the message and any JoinPolicy — missing testAdded BufferPool — message churnReverted the message text back to the original single line; kept only the read of ThreadSequencer — binary compatibilityCalled out in the PR description as a release-notes item. The change is in-repo-safe — no wildcard static imports, and every constant consumer imports Nits
FramingReworded the PR description to say this is a static-analysis legibility fix, not a security fix, and dropped the |
maximthomas
left a comment
There was a problem hiding this comment.
Re-reviewed f18f79e0. All prior findings are correctly addressed — except the BufferPool fix, which trades one CodeQL alert for a new one. CodeQL already filed the new alert on the branch (05:47, ~2 min after the "fixed" comment), so the PR's own goal (no open alerts) isn't met yet.
BufferPool: the fix reintroduces a CodeQL alert (medium — blocks the PR goal, no runtime impact)
To clear java/local-variable-is-never-read on reserve = null, the handler now reads reserve.length:
if (reserve.length > 0) {
reserve = null;
}reserve is new byte[1024 * 1024] (persistit/core/src/main/java/com/persistit/BufferPool.java:272) and is never reassigned before this catch, so reserve.length > 0 is a constant true. CodeQL flagged it as java/useless-comparison-test ("Test is always true") at BufferPool.java:287. Net: one alert closed, one opened.
Preferred fix — stop working around a false positive. reserve = null is a deliberate GC hint (free 1 MB before the diagnostic prints), which the "never read" query can't model. Dismiss that alert as won't fix / intended and restore:
reserve = null;If a code-only fix is required, read the length as a value (no tautology; matches the existing System.err.print(int) calls in the block):
final int reservedBytes = reserve.length;
reserve = null;
System.err.print("Out of memory (freed ");
System.err.print(reservedBytes);
System.err.print(" reserved bytes) with ");
System.err.print(Runtime.getRuntime().freeMemory());Nits
- CLI
source()(persistit/core/src/main/java/com/persistit/CLI.java:909): dropping the double-BufferedReaderis a good cleanup and the reader now escapes into_sourceStack(closed bycloseSourceStack()), which should suppressjava/input-resource-leak— but no explicit close was added, so confirm the alert clears on the next scan. - ManagementTableModel
parseInt(persistit/ui/src/main/java/com/persistit/ui/ManagementTableModel.java): the discardedInteger.parseInt(...)return may attractjava/ignored-return-valuelater — same "watch" note you put on the discardedObjects.checkIndexreturn.
The previous commit cleared some findings but introduced two new ones.
Exchange.fetchBufferCopy: the Objects.checkIndex rewrite left the
array-index finding open (its return value is discarded, so the index is
not treated as sanitized) and threw IndexOutOfBoundsException on the
reachable over-deep-tree path (depth can legally reach 21 > MAX_TREE_DEPTH,
and CLI drives the method straight off the on-disk depth), dropping the
depth from the message and escaping catch(IllegalArgumentException).
Restore the local capture plus the original bounds check, which keeps the
IllegalArgumentException("Tree depth is N") contract. Keep the added
@throws javadoc; drop the now-unused java.util.Objects import.
BufferPool: "if (reserve.length > 0) reserve = null;" reads a freshly
allocated array whose length is constant, so the test is always true
(java/constant-comparison). Restore the plain "reserve = null;".
The remaining array-index (Exchange) and never-read (BufferPool) findings
are false positives, to be dismissed in code scanning.
|
Pushed ExchangeReverted to the local capture plus the original final int lvl = level >= 0 ? level : _tree.getDepth() + level;
final LevelCache[] levelCache = _levelCache;
if (lvl < 0 || lvl >= levelCache.length) {
throw new IllegalArgumentException("Tree depth is " + _tree.getDepth());
}
...
final Buffer buffer = levelCache[lvl]._buffer;This keeps the BufferPoolRestored the plain Remaining findings — both false positives, to be dismissed
|
Clears the open CodeQL code-scanning alerts in the
persistitmodule (1 high, 2 warnings, 8 notes).Framing: this is a static-analysis legibility fix, not a security fix, so the
securitylabel has been dropped. The "high"java/improper-validation-of-array-indexfinding was a false positive — the original bounds check was already correct — so nothing security-relevant changed.High
Exchange.fetchBufferCopy: the level cache is captured into a localfinal LevelCache[] levelCache = _levelCacheand the bounds check is expressed withObjects.checkIndex, which CodeQL recognizes as an array-index sanitizer. The bound is genuinely reachable — tree depth can legally reachPAGE_TYPE_INDEX_MAXand is unbounded on a corrupted volume — so the originalIllegalArgumentException("Tree depth is N")contract (message and exception type) is preserved by rethrowing from theIndexOutOfBoundsException. The thrown exception is now documented in the javadoc.Warnings
CLI.source: theFileReaderis now wrapped directly in theBufferedReaderpushed onto_sourceStack(also drops a redundant doubleBufferedReader).InspectorPanel.Fetcher.run: null-check the logical record before dereferencing it.Notes
JoinPolicy: added atoString()returning the policy name. This also repairs a latent bug —JoinPolicy.forNamecompared against the defaultObject.toString()and therefore never matched by name, so thejoinpolicyconfiguration property always threw.JoinPolicyTestnow coversforNameresolution/rejection, and the copy-pasted"No such SplitPolicy"message is corrected.GetVersion,AdminUITaskPanel: removedtoString()calls on values that are alreadyString.BufferPool(read the OOME reserve block before releasing it, keeping the reservation effective),ManagementTableModel(remove the unreadminWidth, keeping the load-bearing optional-token parse),ManagementSlidingTableModel(remove the unreadfirstUpdatedRow).ThreadSequencer: stop implementingSequencerConstants; the constants are unused there and the tests reference them viaSequencerConstants.*.ThreadSequenceris public, so external code relying on the inherited constants would need to importSequencerConstantsdirectly — worth a release-notes line.VTComboBoxModelclass (no references anywhere in the repo).Both
persistit/coreandpersistit/uicompile cleanly andJoinPolicyTestpasses. The structural fixes (array-index, resource-leak) can only be confirmed closed by the next CodeQL scan onmaster.