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());
+        }
+    }
 }

Reply via email to