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));