You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/10/20 15:01:50 UTC
(maven) branch maven-3.10.x updated: [3.10.x] Fix gh-11618: Make DefaultModelValidator thread-safe (#13303)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch maven-3.10.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-3.10.x by this push:
new bb5f9d767a [3.10.x] Fix gh-11618: Make DefaultModelValidator thread-safe (#13303)
bb5f9d767a is described below
commit bb5f9d767a4f5f1cc3f98e5f43daad9b081d3cb9
Author: Abhishek Chauhan <60...@users.noreply.github.com>
AuthorDate: Tue Oct 20 20:31:42 2026 +0530
[3.10.x] Fix gh-11618: Make DefaultModelValidator thread-safe (#13303)
Backport of #11734 (already on `master`, and on `maven-4.0.x` via #12284) to `maven-3.10.x`. A backport to the 3.x line was requested on #11734 (https://github.com/apache/maven/pull/11734#issuecomment-5158052289) and has not been answered.
### Problem
`DefaultModelValidator` is a `@Singleton`, and its `validIds` cache is a plain `HashSet` shared by every validation that runs through it. Callers that validate models concurrently (the breadth-first dependency collector, `aether.dependencyCollector.impl=bf`, as used by Clojure tools-deps and Leiningen; IDE integrations such as NetBeans) can corrupt the underlying `HashMap`. #11618 reports a `ClassCastException` (`HashMap$Node cannot be cast to class HashMap$TreeNode`).
### Change
Same as on `master` and `maven-4.0.x`:
- `validIds` is `ConcurrentHashMap.newKeySet()`;
- `validateId()` checks `id != null` before `contains()`, since a `ConcurrentHashMap` key set throws on a `null` lookup. A `null` id still reaches `validateStringNotEmpty()` and is reported as before.
The regression test `testConcurrentValidation()` is the one from #11734, unchanged.
### Verification on `maven-3.10.x`
- `mvn -pl maven-model-builder verify` (including spotless and RAT): BUILD SUCCESS, 205 tests (77 in `DefaultModelValidatorTest`), 0 failures.
- `git diff --check` is clean.
- I also stress-tested the singleton directly: 16 threads × 2000 unique models per round, 50 rounds, one shared `DefaultModelValidator`.
- **Without the change:** 3 of 3 runs never finished and were killed after 60 s. A thread dump showed the threads spinning in `HashMap$TreeNode.balanceInsertion()` under `validateId()`. Short single-round runs threw the `ClassCastException` reported in #11618.
- **With the change:** all 150 rounds complete in about 1 s with no failures.
`testConcurrentValidation()` is probabilistic. In my runs a single iteration of that load hit the race about 1 time in 8, so it guards against the bug coming back but will not fail on every run against the unfixed code.
- [x] I hereby declare this contribution to be licenced under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0)
Refs #11618
---
.../model/validation/DefaultModelValidator.java | 7 ++-
.../validation/DefaultModelValidatorTest.java | 64 ++++++++++++++++++++++
2 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
index bb8389d28d..687cdd58c1 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
@@ -34,6 +34,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
@@ -92,7 +93,9 @@ public class DefaultModelValidator implements ModelValidator {
private static final String EMPTY = "";
- private final Set<String> validIds = new HashSet<>();
+ // Thread-safe set required because class is @Singleton and validIds is accessed concurrently
+ // See: https://github.com/apache/maven/issues/11618
+ private final Set<String> validIds = ConcurrentHashMap.newKeySet();
private ModelVersionProcessor versionProcessor;
@@ -1159,7 +1162,7 @@ private boolean validateId(
String id,
String sourceHint,
InputLocationTracker tracker) {
- if (validIds.contains(id)) {
+ if (id != null && validIds.contains(id)) {
return true;
}
if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
index a72946ed5e..e662a9d540 100644
--- a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
+++ b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
@@ -21,6 +21,9 @@
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.apache.maven.model.Model;
@@ -975,4 +978,65 @@ public void testOkParentRelativePath1() throws Exception {
SimpleProblemCollector result = validateRaw("raw-model/ok-parent-relativePath1.xml");
assertViolations(result, 0, 0, 0);
}
+
+ /**
+ * Validates thread-safety of DefaultModelValidator during concurrent model validation.
+ *
+ * <p>This test addresses GitHub issue #11618 where concurrent access to a shared
+ * {@code HashSet} in {@code DefaultModelValidator} could cause {@code ClassCastException}.
+ * The underlying issue occurs when multiple threads access a non-thread-safe {@code HashSet}
+ * (backed by {@code HashMap}) during internal restructuring operations.
+ *
+ * <p>The fix replaces {@code HashSet} with {@code ConcurrentHashMap.newKeySet()} to provide
+ * thread-safe concurrent access without external synchronization.
+ *
+ * @see <a href="https://github.com/apache/maven/issues/11618">GitHub #11618</a>
+ */
+ @Test
+ public void testConcurrentValidation() throws Exception {
+ int threadCount = 50;
+ int iterationsPerThread = 1000;
+ CountDownLatch startLatch = new CountDownLatch(1);
+ CountDownLatch doneLatch = new CountDownLatch(threadCount);
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ // Create multiple threads that will validate models concurrently
+ for (int t = 0; t < threadCount; t++) {
+ final int threadId = t;
+ Thread thread = new Thread(() -> {
+ try {
+ startLatch.await(); // Wait for all threads to be ready
+ for (int i = 0; i < iterationsPerThread; i++) {
+ Model model = new Model();
+ model.setModelVersion("4.0.0");
+ model.setGroupId("test.group" + threadId);
+ model.setArtifactId("test-artifact-" + threadId + "-" + i);
+ model.setVersion("1.0.0");
+
+ SimpleProblemCollector problems = new SimpleProblemCollector(model);
+ validator.validateEffectiveModel(model, new DefaultModelBuildingRequest(), problems);
+ }
+ } catch (Throwable e) {
+ failure.compareAndSet(null, e);
+ } finally {
+ doneLatch.countDown();
+ }
+ });
+ thread.setName("validator-test-" + threadId);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Start all threads simultaneously
+ startLatch.countDown();
+
+ // Wait for all threads to complete
+ assertTrue(doneLatch.await(30, TimeUnit.SECONDS), "Threads did not complete in time");
+
+ // Check if any thread encountered an error
+ if (failure.get() != null) {
+ throw new AssertionError(
+ "Concurrent validation failed: " + failure.get().getMessage(), failure.get());
+ }
+ }
}