You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/17 16:34:38 UTC

(maven) branch master updated: Fix #13084 follow-up: fix mutable map leak in compat sandbox context, pre-filter missing()-condition profiles in CONSUMER_DEPENDENCY builds (#13164)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new cce49bc243 Fix #13084 follow-up: fix mutable map leak in compat sandbox context, pre-filter missing()-condition profiles in CONSUMER_DEPENDENCY builds (#13164)
cce49bc243 is described below

commit cce49bc2430a09b2c89a2ceaf13f252ac05726d7
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Sep 17 18:34:31 2026 +0200

    Fix #13084 follow-up: fix mutable map leak in compat sandbox context, pre-filter missing()-condition profiles in CONSUMER_DEPENDENCY builds (#13164)
    
    Forward-port of findings from the review of PR #13158 (4.0.x backport):
    
    - compat: externalActivationContext() returned mutable map from getSystemProperties()
      on both the empty-projectProps branch (raw delegate.getSystemProperties()) and the
      non-empty branch (unwrapped HashMap). Wrap both with Collections.unmodifiableMap().
    
    - impl: the nonFileProfiles pre-filter only excluded profiles with file activation.
      A condition profile using missing(path) passes the filter and enters the sandbox
      where context.exists() always returns false, so missing() = !false = true fires
      unconditionally — the same footgun as <file><missing>. Add hasFileConditionExpression()
      helper and extend the filter to also exclude condition profiles containing exists()/missing().
    
    Tests: extended testProjectBuildEvaluatesAllActivators (missing-condition fires in
    project build) and testDependencyModelActivatesOnlyEnvironmentIndependentProfiles
    (missing-condition suppressed in CONSUMER_DEPENDENCY).
---
 .../maven/model/building/DefaultModelBuilder.java   |  9 +++++----
 .../maven/impl/model/DefaultModelBuilder.java       | 21 ++++++++++++++++++++-
 .../maven/impl/model/DefaultModelBuilderTest.java   |  7 +++++++
 .../poms/factory/resolved-model-with-profiles.xml   | 12 ++++++++++++
 4 files changed, 44 insertions(+), 5 deletions(-)

diff --git a/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
index 97f4291ce9..20179b80c2 100644
--- a/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
+++ b/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
@@ -567,11 +567,12 @@ public Map<String, String> getSystemProperties() {
                 }
                 Map<String, String> projectProps = delegate.getProjectProperties();
                 if (projectProps == null || projectProps.isEmpty()) {
-                    return delegate.getSystemProperties();
+                    mergedSystemProperties = Collections.unmodifiableMap(delegate.getSystemProperties());
+                } else {
+                    Map<String, String> merged = new HashMap<>(projectProps);
+                    merged.putAll(delegate.getSystemProperties()); // system wins
+                    mergedSystemProperties = Collections.unmodifiableMap(merged);
                 }
-                Map<String, String> merged = new HashMap<>(projectProps);
-                merged.putAll(delegate.getSystemProperties()); // system wins
-                mergedSystemProperties = merged;
                 return mergedSystemProperties;
             }
 
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
index d698c5dddb..8183eb5813 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
@@ -1671,9 +1671,12 @@ private List<Profile> getActiveProfiles(
                     // project → dep1 → dep2 pattern. See #13100.
                     // TODO(#13146): repositories contributed by external-model profiles can shadow
                     // central; a WARN/FAIL policy for URL mismatches should be added separately.
+                    // Condition profiles using exists()/missing() are also pre-filtered for the
+                    // same reason as file profiles: in the sandbox, context.exists() always returns
+                    // false, so missing(path) evaluates to !false = true and fires unconditionally.
                     Collection<Profile> nonFileProfiles = interpolatedProfiles.stream()
                             .filter(p -> p.getActivation() == null
-                                    || p.getActivation().getFile() == null)
+                                    || (p.getActivation().getFile() == null && !hasFileConditionExpression(p)))
                             .toList();
                     ProfileActivationContext externalContext =
                             profileActivationContext.withoutUserPropertiesAndFilesystem();
@@ -1719,6 +1722,22 @@ private static boolean hasFileOrPropertyOrConditionActivation(Profile profile) {
                                     && !activation.getCondition().isBlank()));
         }
 
+        /**
+         * Returns {@code true} if the profile's condition expression calls {@code exists()} or
+         * {@code missing()}.  Such profiles must be pre-filtered out when building external
+         * (repository-resolved) models: inside the sandbox, {@code context.exists()} always
+         * returns {@code false}, so {@code missing(path)} evaluates to {@code !false = true}
+         * and fires unconditionally — the same footgun that file-activated profiles expose.
+         */
+        private static boolean hasFileConditionExpression(Profile profile) {
+            Activation a = profile.getActivation();
+            if (a == null || a.getCondition() == null) {
+                return false;
+            }
+            String c = a.getCondition();
+            return c.contains("exists(") || c.contains("missing(");
+        }
+
         /**
          * Packaging-activated profiles are handled separately by the consumer POM builder's
          * {@code inlinePackagingActivatedProfiles()} and must not be activated during
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
index 377e78cbd3..9a5af0e462 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
@@ -170,6 +170,9 @@ public void testProjectBuildEvaluatesAllActivators() {
         assertEquals("activated", model.getProperties().get("profile.file"));
         assertEquals("activated", model.getProperties().get("profile.property"));
         assertEquals("activated", model.getProperties().get("profile.condition"));
+        // missing('/publisher/private/file') fires in a project build because that path
+        // genuinely doesn't exist on the developer's machine — normal project-build behaviour.
+        assertEquals("activated", model.getProperties().get("profile.missing.condition"));
         assertEquals("activated", model.getProperties().get("profile.jdk"));
         assertEquals("activated", model.getProperties().get("profile.negated.property"));
         assertTrue(model.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
@@ -187,6 +190,10 @@ public void testDependencyModelActivatesOnlyEnvironmentIndependentProfiles() {
         assertNull(model.getProperties().get("profile.file"));
         assertNull(model.getProperties().get("profile.property"));
         assertNull(model.getProperties().get("profile.condition"));
+        // missing()-condition profiles must be pre-filtered in CONSUMER_DEPENDENCY builds:
+        // in the sandbox context.exists() always returns false, so missing(path) evaluates
+        // to !false = true and fires unconditionally — same footgun as <file><missing>.
+        assertNull(model.getProperties().get("profile.missing.condition"));
         assertEquals("activated", model.getProperties().get("profile.jdk"));
         // Negated-property profile (!skip.defaults) fires because 'skip.defaults' is absent
         // from the sandboxed context (user properties are suppressed; system properties do not
diff --git a/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml b/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml
index e3c53aefed..fd30698c24 100644
--- a/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml
+++ b/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml
@@ -52,6 +52,18 @@
                 <profile.condition>activated</profile.condition>
             </properties>
         </profile>
+        <!-- missing()-condition profile: must NOT fire in CONSUMER_DEPENDENCY builds -->
+        <!-- missing() = !exists() and in the sandbox exists() always returns false,  -->
+        <!-- so missing() would always return true — this profile must be pre-filtered. -->
+        <profile>
+            <id>missing-condition</id>
+            <activation>
+                <condition>missing('/publisher/private/file')</condition>
+            </activation>
+            <properties>
+                <profile.missing.condition>activated</profile.missing.condition>
+            </properties>
+        </profile>
         <!-- default-on profile: fires when 'skip.defaults' is absent; suppressed when set as system property -->
         <profile>
             <id>negated-property-default-on</id>