This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new 3744153a37 Fix thread-safety in DefaultModelValidator (#11734) (#12284)
3744153a37 is described below
commit 3744153a3784fcbb24f5c0dee0d5df4521dfca5b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jun 17 12:05:13 2026 +0200
Fix thread-safety in DefaultModelValidator (#11734) (#12284)
Replace the non-thread-safe HashSet used for caching validated IDs with
ConcurrentHashMap.newKeySet() in the compat-layer DefaultModelValidator.
The class is a @Singleton, but its validIds field was a plain HashSet
that could be concurrently mutated by multiple threads during model
validation (e.g. when using the breadth-first dependency collector).
This caused ClassCastException when HashMap internally tree-ifies
buckets while another thread reads.
The fix aligns with the Maven 4 maven-impl module which already uses
ConcurrentHashMap.newKeySet() for this purpose. A null-guard is also
added before the contains() check since ConcurrentHashMap does not
permit null keys.
Includes a concurrent validation regression test.
Fixes #11618
Co-authored-by: Abhishek Chauhan <[email protected]>
---
.../model/validation/DefaultModelValidator.java | 7 ++-
.../validation/DefaultModelValidatorTest.java | 64 ++++++++++++++++++++++
2 files changed, 69 insertions(+), 2 deletions(-)
diff --git
a/compat/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
b/compat/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
index 767054b9bd..20e04147c6 100644
---
a/compat/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
+++
b/compat/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;
@@ -89,7 +90,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;
@@ -1125,7 +1128,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/compat/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
b/compat/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
index d856b6ae94..4d6c3faf1b 100644
---
a/compat/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
+++
b/compat/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
@@ -22,6 +22,9 @@
import java.io.Serial;
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;
@@ -894,4 +897,65 @@ void profileActivationPropertyWithProjectExpression()
throws Exception {
+ "${project.version} expressions are not supported
during profile activation.",
result.getWarnings().get(1));
}
+
+ /**
+ * 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
+ void testConcurrentValidation() throws Exception {
+ int threadCount = 10;
+ int iterationsPerThread = 100;
+ 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());
+ }
+ }
}