Repository navigation
SONARJAVA-7154: Implemented rule S9428 MessageDigest instances should not be stored as fields - #6321
romainbrenguier wants to merge 7 commits into
Conversation
… not be stored as fields Detects MessageDigest instances retained in type fields, which can share mutable digest state across invocations or threads.
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 2 Pipeline jobs failed
Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: ae92bb2 | Docs | View more details | Give us feedback! |
Add the required constructor to CustomDigest, avoid checked exceptions in a java.util.function lambda, and use numeric CWE ids in the rule metadata. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
⚖️ Ruling update ready for review: #6326 |
Do not report private MessageDigest fields whose usages are only assignments, state-independent calls such as clone(), or located in synchronized code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ruling Diff SummaryDetected changes in 2 rule files: 2 issues removed, 2 issues added. S9428 (
|
…tion and restrict stateless-call exemption A private field is now ignored only if all its non-assignment usages are state-independent calls (prototype pattern), or are all guarded by the same known monitor, which must be shared by all instances for static fields. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ses through this Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
/pvf |
|
📊 Performance Validation complete |
rombirli
left a comment
There was a problem hiding this comment.
This rule looks good!
REQUEST CHANGE: issue location in unit tests
NIT:
- Use matchers instead of raw strings
- I would like to understand further the heuristic choices before merging, some of them are too heavy to me and might not be necessary, we could greatly simplify rule implementation and reproducer.
| private MessageDigest requestDigest; // Noncompliant {{Make this MessageDigest a local variable.}} | ||
| private static MessageDigest sharedDigest; // Noncompliant {{Make this MessageDigest a local variable.}} | ||
| private final MessageDigest configuredDigest; // Noncompliant {{Make this MessageDigest a local variable.}} | ||
| private CustomDigest customDigest; // Noncompliant {{Make this MessageDigest a local variable.}} | ||
| private String digest = "SHA-256"; // Compliant | ||
| private ThreadLocal<MessageDigest> threadDigest = new ThreadLocal<>(); // Compliant | ||
| private Object deferredDigest; // Compliant |
There was a problem hiding this comment.
We should add location markers ^^^^^^^^ in the tests. Currently the issue reports only on the field name:
private final MessageDigest configuredDigest; // Noncompliant {{Make this MessageDigest a local variable.}}
// ^^^^^^^^^^^^^^^^it would make more sense to me to report on the whole field declaration.
private final MessageDigest configuredDigest; // Noncompliant {{Make this MessageDigest a local variable.}}
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^| private static MessageDigest lockedDigest; // Compliant, every usage is synchronized | ||
| private MessageDigest methodDigest; // Compliant, every usage is synchronized |
There was a problem hiding this comment.
I'm not entirely sure the heuristic every usage is synchronized is great.
For computing a few digest occasionally, instantiating a new instance every time is more efficient. For higher loads it's recommended to use a ThreadLocal<MessageDigest>. Tracking usages also makes the rule more complexe.
| .addWithoutParametersMatcher() | ||
| .build(); | ||
|
|
||
| private boolean hasSemantic; |
There was a problem hiding this comment.
This hasSemantic could be removed:
- It is not needed at all, if semantic is not available, the
MESSAGE_DIGESTwill never match -> it is useless logic/code. - It makes the rule stateful even if it only stores a boolean and there is no risk of memory leak. In general it's much better to have only constant fields in rules.
| VariableTree variable = (VariableTree) tree; | ||
| Symbol symbol = variable.symbol(); | ||
| Type type = symbol.type(); | ||
| if (symbol.owner().isTypeSymbol() && !type.isUnknown() && type.isSubtypeOf(MESSAGE_DIGEST) && !isOnlyUsedSafely(symbol)) { |
There was a problem hiding this comment.
could we use a constant matcher :
private static final MethodMatchers.create()
.ofSubTypes(MESSAGE_DIGEST);instead of
type.isSubtypeOf(MESSAGE_DIGEST)The part !type.isUnknown() is useless as well as an unknown type will not match the MESSAGE_DIGEST FQN
There was a problem hiding this comment.
I removed !type.isUnknown(), since an unknown type never matches. I kept type.isSubtypeOf(MESSAGE_DIGEST) because MethodMatchers matches method invocations and references, not the type of a variable. ofSubTypes only sets the owner type criterion of a method matcher, so there is no method call to match here.
| List<IdentifierTree> readUsages = symbol.usages().stream() | ||
| .filter(usage -> !isAssignedTo(usage)) | ||
| .toList(); |
There was a problem hiding this comment.
This looks to cover only aliasing the MessageDigest objects. What about if a MessageDigest escapes a function either by giving it to another function or returning it?
public void f(){
g(myMessageDigest); // escapes the function
return myMessageDigest; // escapes the function
}There was a problem hiding this comment.
Escaping is already reported. Only usages that are assignment targets or receivers of state-independent calls (clone(), getAlgorithm(), ...) are exempt, so passing the field to g(md), returning it, or aliasing it all lead to an issue. I added EscapingPrototypeHolder to the sample to cover these cases.
| VariableTree variable = (VariableTree) tree; | ||
| Symbol symbol = variable.symbol(); | ||
| Type type = symbol.type(); | ||
| if (symbol.owner().isTypeSymbol() && !type.isUnknown() && type.isSubtypeOf(MESSAGE_DIGEST) && !isOnlyUsedSafely(symbol)) { |
There was a problem hiding this comment.
Could we also try running ruling/pvf without the && !isOnlyUsedSafely(...) part. I'm curious how much these heuristics filter out issues. If they have no impact I would be for keeping this rule simple and removing everything after line 79
There was a problem hiding this comment.
I checked the ruling sources. There are only 3 MessageDigest fields. The synchronized heuristic filtered none of them (Jetty Credential.MD5.__md uses an AutoLock and was already reported). So I removed the whole monitor tracking. The prototype heuristic filters 1 issue: Guava MessageDigestHashFunction.prototype, which is only used with clone()/getAlgorithm()/getDigestLength(). That is a real false positive and a common pattern, so I kept this small check.
…laration Drop the synchronized-usage exemption, which filtered no ruling issue and contradicts the rule description discouraging synchronization of shared digests. Keep only the prototype exemption, remove the semantic guard and report on the whole variable declaration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
⚖️ Ruling update ready for review: #6342 |
| /** | ||
| * A private field is only used as a prototype when, apart from being assigned, it is only the receiver of calls which do not | ||
| * depend on or alter the digest state, such as {@code clone()} or {@code getAlgorithm()}. | ||
| */ | ||
| private static boolean isOnlyUsedAsPrototype(Symbol symbol) { | ||
| return symbol.isPrivate() && symbol.usages().stream() | ||
| .map(MessageDigestInstanceFieldCheck::fieldExpression) | ||
| .allMatch(expression -> isAssignedTo(expression) || isReceiverOfStatelessCall(expression)); | ||
| } |
There was a problem hiding this comment.
💡 Quality: PR description still promises a synchronized exemption the rule dropped
This commit removes all monitor handling from isOnlyUsedAsPrototype. Private fields used only inside synchronized blocks or methods are now reported, and the sample's SynchronizedHolder.lockedDigest/methodDigest are now marked Noncompliant. The PR description still says private fields are not reported when each usage "sits in a synchronized block or method". It also explains the jetty ruling hit as "guarded by a custom AutoLock rather than synchronized", which no longer matters, since any synchronized-only field is now reported too. The ruling expectations were recorded while the synchronized exemption was in place (commit e73dd8c) and this commit doesn't touch them, so the description and the ruling rationale no longer describe what the rule does. Fix: update the PR description, and re-run the ruling to confirm that no synchronized-only fields in the ruling projects now produce new issues.
Was this helpful? React with 👍 / 👎
CI failed: Ruling QA failed on two fronts: the new S9428 rule's output likely diverges from the Eclipse Jetty and Guava expected ruling snapshots (change-related, medium confidence), and a Jetty expected entry for Credential.java was not analyzed while the Guava ruling test errored with its cause missing from the logs (relation unclear).OverviewTwo unique error templates across 12 analyzed logs from the Ruling QA jobs (Linux and Windows). Cache, runner, and setup steps completed without error. The real failures are in the ruling integration tests, which compare actual analyzer output against expected snapshots. FailuresRuling snapshot mismatch for S9428 (confidence: medium)
Jetty expected file not analyzed and Guava ruling test error (confidence: medium)
Summary
Code Review 👍 Approved with suggestions 4 closed / 5 findings🟡 Medium risk · Adding S9428 changes Java analysis results by flagging retained MessageDigest fields; imperfect usage exemptions could miss unsafe shared digest state or flag safe fields. Adds rule S9428 to flag 💡 Quality: PR description still promises a synchronized exemption the rule dropped📄 java-checks/src/main/java/org/sonar/java/checks/MessageDigestInstanceFieldCheck.java:57-65 📄 java-checks-test-sources/default/src/main/java/checks/MessageDigestInstanceFieldCheckSample.java:118-132 This commit removes all monitor handling from ✅ 4 closed✅ Bug: Synchronized exemption ignores which monitor is held
✅ Bug: clone()/toString() exemption hides races with synchronized updates
Closed: Bug: Test sample won't compile: CustomDigest has no matching super ctor
Closed: Edge Case: Instance-monitor exemption ignores which instance owns the field
🤖 Prompt for agentsVerification Knowledge ✅ 1 Repos 3 inspectedFunctional validation No results Rules No rules evaluated Knowledge ✅ 1 expectation metCross-repo 3 repositories inspected 0 open impacts
Auto-approval Not enabledTip Comment OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
|
|
⚖️ Ruling update ready for review: #6342 |




Detects MessageDigest instances retained in type fields, which can share mutable digest state across invocations or threads.
Private fields are not reported when each usage is an assignment, a call that does not depend on the digest state (
clone(),getAlgorithm(),getDigestLength(),getProvider(),toString()), or sits in asynchronizedblock or method. This covers the prototype-for-cloning pattern (guavaMessageDigestHashFunction.prototype).Ruling: 2 expected issues (guava
MessageDigestHashFunction.java:121, eclipse-jettyCredential.java:190, which is guarded by a customAutoLockrather thansynchronized).Agent workflow
PR created using
uv run new_rule_implementation.py S9428 -j SONARJAVA-7154 -a codex -g AGENTS.md .claude/skills/new-rule/SKILL.mdTool link: https://github.com/SonarSource/languages-experimental-tooling/tree/romain/my-tickets/personal/romain-brenguier
PR created using
uv run create_with_claude.py /tmp/action_plan_romain/new-rule-s9428-sonarjava-7154.txtSummary by Gitar
clone(),or inside asynchronizedcontextMessageDigestInstanceFieldCheckSampleand verifier tests, including a run without semanticsThis will update automatically on new commits.
🤖 Generated with Claude Code
PR created using
uv run create_with_claude.py /tmp/action_plan_romain/new-rule-s9428-sonarjava-7154.txtPR created using
uv run create_with_claude.py /tmp/action_plan_romain/new-rule-s9428-sonarjava-7154.txtPR created using
uv run create_with_claude.py /tmp/action_plan_romain/new-rule-s9428-sonarjava-7154.txtIterated on the PR with
uv run ci_loop.pyfor 4 iterations.✔️ The PR is now ready for review.
Addressed review comments in pr_report_6321.md using
uv run address_reviews.py pr_report_6321.mdPR updated using
uv run update_with_claude.py --prompt "Implement the action plan described in the document All the non-compliant examples in java-checks-test-sources/default/src/main/java/checks/MessageDigestInstanceFieldCheckSample.java should have caret markers to show where the issue is raised. Some are missing currently.." -a "All the non-compliant examples in java-checks-test-sources/default/src/main/java/checks/MessageDigestInstanceFieldCheckSample.java should have caret markers to show where the issue is raised. Some are missing currently." -g "claude"