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/09/08 15:24:43 UTC

(maven-resolver) branch master updated: fix: accept legacy ID-only tracking entries via backward-compatible fallback (#2133)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 18753daa3 fix: accept legacy ID-only tracking entries via backward-compatible fallback (#2133)
18753daa3 is described below

commit 18753daa30739a699ea56e30865e8de7f4e58369
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Tue Sep 8 17:24:38 2026 +0200

    fix: accept legacy ID-only tracking entries via backward-compatible fallback (#2133)
    
    ## Summary
    
    When the tracking key function is URL-qualified (`nid_hurl`, the default since 2.0.23 / commit 339161b), tracking entries written by an older resolver using ID-only keys (`nid` format, e.g. `artifact>central=`) are invisible to the new lookup which expects `artifact>central-<sha1>=`.
    
    This causes **all** artifacts cached in the local repository before the upgrade to appear as "present but unavailable", triggering full re-downloads from remote repositories. In CI environments and integration tests that use fake/file-based repositories (like maven-integration-testing), this breaks resolution entirely — see [apache/maven#13078 (comment)](https://github.com/apache/maven/pull/13078#issuecomment-5583775310).
    
    ## Root Cause
    
    The tracking key change from `nid` to `nid_hurl` (finding f013 in the security audit) makes existing `_remote.repositories` entries invisible:
    
    - **Old entry**: `maven-core-3.8.6.pom>central=`
    - **New lookup**: `maven-core-3.8.6.pom>central-<sha1 of repo URL>=` → no match
    
    The `isTracked()` check finds the old `central=` entry, so the artifact is treated as "tracked but not for the current repo" rather than "untracked" — the untracked inter-op fallback does not apply, and the artifact is rejected.
    
    ## Fix
    
    Add a backward-compatible fallback in `applyTracking()`: when the URL-qualified (`nid_hurl`) lookup misses, try the system-wide key function (`nid` by default) as a fallback. If that matches, accept the artifact and log a debug message. The legacy entry will be upgraded to the new key format on the next download.
    
    This preserves the f013 security improvement (URL-qualified tracking prevents same-id-different-URL repository poisoning for *new* entries) while providing a smooth upgrade path from older resolvers.
    
    ## Tests
    
    - Updated `testUrlQualifiedTrackingTreatsLegacyIdOnlyEntriesAsStale` → `testUrlQualifiedTrackingAcceptsLegacyIdOnlyEntriesViaFallback`: legacy entries from a matching repo are now accepted
    - Added `testUrlQualifiedTrackingRejectsLegacyIdOnlyEntriesFromDifferentRepo`: legacy entries from a *different* repo are still rejected
    - `testUrlQualifiedTrackingDistinguishesSameIdDifferentUrl`: unchanged — URL-qualified entries still prevent same-id-different-URL poisoning
---
 .../impl/EnhancedLocalRepositoryManager.java       | 28 ++++++++++++++++++++-
 .../EnhancedLocalRepositoryManagerFactory.java     | 21 ++++++++++++++++
 .../impl/EnhancedLocalRepositoryManagerTest.java   | 29 +++++++++++++++++++---
 .../EnhancedSplitLocalRepositoryManagerTest.java   |  1 +
 4 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index 1550c875c..d5bf280d0 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -97,6 +97,8 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
 
     private final String trackingFilename;
 
+    private final boolean legacyTrackingFallback;
+
     private final TrackingFileManager trackingFileManager;
 
     private final LocalPathPrefixComposer localPathPrefixComposer;
@@ -135,18 +137,21 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
      */
     private final Path realBasePath;
 
+    @SuppressWarnings("checkstyle:parameternumber")
     EnhancedLocalRepositoryManager(
             Path basedir,
             LocalPathComposer localPathComposer,
             RepositoryKeyFunction repositoryKeyFunction,
             RepositoryKeyFunction trackingRepositoryKeyFunction,
             String trackingFilename,
+            boolean legacyTrackingFallback,
             TrackingFileManager trackingFileManager,
             LocalPathPrefixComposer localPathPrefixComposer)
             throws IOException {
         super(basedir, "enhanced", localPathComposer, repositoryKeyFunction);
         this.trackingRepositoryKeyFunction = requireNonNull(trackingRepositoryKeyFunction);
         this.trackingFilename = requireNonNull(trackingFilename);
+        this.legacyTrackingFallback = legacyTrackingFallback;
         this.trackingFileManager = requireNonNull(trackingFileManager);
         this.localPathPrefixComposer = requireNonNull(localPathPrefixComposer);
         // a fresh local repository does not exist yet; toRealPath() requires it to
@@ -294,13 +299,34 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
         }
         String context = result.getRequest().getContext();
         for (RemoteRepository repository : result.getRequest().getRepositories()) {
-            if (props.get(getKey(path, getTrackingRepositoryKey(repository, context))) != null) {
+            String trackingKey = getTrackingRepositoryKey(repository, context);
+            if (props.get(getKey(path, trackingKey)) != null) {
                 // artifact downloaded from remote repository is accepted only downloaded from request
                 // repositories
                 result.setAvailable(true);
                 result.setRepository(repository);
                 return true;
             }
+            if (legacyTrackingFallback) {
+                // Backward compatibility fallback: if the tracking key function is URL-qualified (e.g. nid_hurl)
+                // but the tracking file was written by an older resolver using the system-wide key function
+                // (e.g. nid, producing ID-only entries like "artifact>central="), the URL-qualified lookup above
+                // misses. Try the system-wide key function as a fallback: if it matches, the artifact was genuinely
+                // downloaded from this repository under the old key scheme. Accept it and log a migration notice.
+                String legacyKey = getRepositoryKey(repository, context);
+                if (!legacyKey.equals(trackingKey) && props.get(getKey(path, legacyKey)) != null) {
+                    LOGGER.debug(
+                            "Accepting locally cached artifact {} via legacy tracking key '{}'"
+                                    + " (current key function would produce '{}'); the entry will be"
+                                    + " upgraded on next download",
+                            path.getFileName(),
+                            legacyKey,
+                            trackingKey);
+                    result.setAvailable(true);
+                    result.setRepository(repository);
+                    return true;
+                }
+            }
         }
         return false;
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
index 6587a9166..ae361cb05 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
@@ -106,6 +106,24 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
 
     public static final boolean DEFAULT_VERIFY_REAL_PATH = true;
 
+    /**
+     * Whether to enable "legacy tracking fallback" in LRM. If starting "greenfield" with Resolver 2 enabled Maven,
+     * this should be {@code false}, but for smoother transition of users using Maven 3.9 or older versions, the default
+     * is {@code true}. When the local repository is shared across "older" and "newer" Maven versions (where "older"
+     * Maven versions are Resolver 1.x and "never" Maven versions are Resolver 2.x based), the preferred way is to
+     * enable this feature. On the other hand, if local repository is exclusively used by "newer" Maven versions,
+     * like 3.10 or above, for improved Repository cache poisoning protection, this configuration is recommended
+     * to be set to {@code false}.
+     *
+     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
+     * @configurationType {@link java.lang.Boolean}
+     * @configurationDefaultValue {@link #DEFAULT_LEGACY_TRACKING_FALLBACK}
+     * @since 2.0.23
+     */
+    public static final String CONFIG_PROP_LEGACY_TRACKING_FALLBACK = CONFIG_PROPS_PREFIX + "legacyTrackingFallback";
+
+    public static final boolean DEFAULT_LEGACY_TRACKING_FALLBACK = true;
+
     private float priority = 10.0f;
 
     private final LocalPathComposer localPathComposer;
@@ -141,6 +159,8 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
                 || trackingFilename.contains("..")) {
             trackingFilename = DEFAULT_TRACKING_FILENAME;
         }
+        boolean legacyTrackingFallback =
+                ConfigUtils.getBoolean(session, DEFAULT_LEGACY_TRACKING_FALLBACK, CONFIG_PROP_LEGACY_TRACKING_FALLBACK);
 
         if ("".equals(repository.getContentType()) || "default".equals(repository.getContentType())) {
             try {
@@ -157,6 +177,7 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
                                         ConfigurationProperties.REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION),
                                 CONFIG_PROP_TRACKING_REPOSITORY_KEY_FUNCTION),
                         trackingFilename,
+                        legacyTrackingFallback,
                         trackingFileManager,
                         localPathPrefixComposerFactory.createComposer(session));
             } catch (IOException e) {
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
index 2ba597b1a..e53e774e5 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
@@ -114,6 +114,7 @@ public class EnhancedLocalRepositoryManagerTest {
                 RepositoryIdHelper::simpleRepositoryKey,
                 RepositoryIdHelper::simpleRepositoryKey,
                 "_remote.repositories",
+                true,
                 trackingFileManager,
                 new DefaultLocalPathPrefixComposerFactory(new DefaultRepositoryKeyFunctionFactory())
                         .createComposer(session));
@@ -413,6 +414,7 @@ public class EnhancedLocalRepositoryManagerTest {
                 RepositoryIdHelper::simpleRepositoryKey,
                 RepositoryIdHelper.getRepositoryKeyFunction("nid_hurl"),
                 "_remote.repositories",
+                true,
                 trackingFileManager,
                 new DefaultLocalPathPrefixComposerFactory(new DefaultRepositoryKeyFunctionFactory())
                         .createComposer(session));
@@ -440,7 +442,7 @@ public class EnhancedLocalRepositoryManagerTest {
     }
 
     @Test
-    void testUrlQualifiedTrackingTreatsLegacyIdOnlyEntriesAsStale() throws Exception {
+    void testUrlQualifiedTrackingAcceptsLegacyIdOnlyEntriesViaFallback() throws Exception {
         manager = newUrlQualifiedTrackingManager();
 
         // artifact file present, tracked under a legacy ID-only key as written by an older resolver
@@ -451,9 +453,28 @@ public class EnhancedLocalRepositoryManagerTest {
                 Collections.singletonMap(
                         file.getName() + ">" + RepositoryIdHelper.simpleRepositoryKey(repository, testContext), ""));
 
-        // fail-safe stale-key semantics: the legacy key matches no request repository, and because the
-        // file IS tracked it must not fall through to the untracked inter-op acceptance either — the
-        // artifact is simply unavailable locally and gets re-fetched (with checksum validation)
+        // backward-compatible fallback: the legacy ID-only key matches the system-wide key function
+        // for the request repository, so the artifact is accepted (avoiding a full re-download) and
+        // the origin repository is correctly reported
+        LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext);
+        LocalArtifactResult result = manager.find(session, request);
+        assertTrue(result.isAvailable());
+        assertEquals(repository, result.getRepository());
+    }
+
+    @Test
+    void testUrlQualifiedTrackingRejectsLegacyIdOnlyEntriesFromDifferentRepo() throws Exception {
+        manager = newUrlQualifiedTrackingManager();
+
+        // artifact file present, tracked under a legacy ID-only key for a DIFFERENT repository
+        copy(artifact, manager.getPathForLocalArtifact(artifact));
+        File file = new File(basedir, manager.getPathForLocalArtifact(artifact));
+        trackingFileManager.update(
+                new File(file.getParentFile(), "_remote.repositories").toPath(),
+                Collections.singletonMap(file.getName() + ">some-other-repo", ""));
+
+        // even with legacy fallback, an entry for a different repository id must not be accepted:
+        // the artifact is tracked (not untracked) and none of the keys match — unavailable
         LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext);
         assertFalse(manager.find(session, request).isAvailable());
     }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
index 116f9f13d..d33466408 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
@@ -39,6 +39,7 @@ public class EnhancedSplitLocalRepositoryManagerTest extends EnhancedLocalReposi
                 RepositoryIdHelper::simpleRepositoryKey,
                 RepositoryIdHelper::simpleRepositoryKey,
                 "_remote.repositories",
+                true,
                 trackingFileManager,
                 new DefaultLocalPathPrefixComposerFactory(new DefaultRepositoryKeyFunctionFactory())
                         .createComposer(session));