Skip to content

Resolver 2.0.23 and validation cleanup - #13078

Open
cstamas wants to merge 5 commits into
apache:maven-3.10.xfrom
cstamas:maven-3.10.x-resolver-2023
Open

cstamas wants to merge 5 commits into
apache:maven-3.10.xfrom
cstamas:maven-3.10.x-resolver-2023

Conversation

@cstamas

@cstamas cstamas commented Sep 8, 2026

Copy link
Copy Markdown
Member

We had overlapping changes in Resolver and Maven, align them.

We had overlapping changes in Resolver and Maven,
align them.
@cstamas cstamas added this to the 3.10.0 milestone Sep 8, 2026
@cstamas cstamas self-assigned this Sep 8, 2026
@cstamas cstamas added the dependencies Pull requests that update a dependency file label Sep 8, 2026
@ascheman

ascheman commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Sure about the SNAPSHOT-dependency, @cstamas ? Or will you resolve this before you make the draft final?

@cstamas

cstamas commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

We usually run "main" branches (in quotes, as we have several "main" branches, the master, the maven-4.0.x, the maven-3.10.x, etc) just before the release with SNAPSHOT versions, as we do produce and deploy them, is fine to get another layer of confirmation. OTOH, you cannot release anyway with a SNAPSHOT, hence, this is completely okay, and we did it before as well.

@cstamas

cstamas commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

So, all of ITs explode, reason is f013 in this commit:
apache/maven-resolver@339161b

@gnodet ping

@gnodet

gnodet commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

@cstamas The root cause is the tracking key function change from nid to nid_hurl in commit 339161b (f013). All existing _remote.repositories entries use ID-only keys (e.g. artifact>central=), but the new resolver looks for URL-qualified keys (artifact>central-<sha1>=). No match → every cached artifact appears "present but unavailable" → re-download from the IT's fake repos → 💥

This affects all 9 IT jobs (4229 test failures, 507 unique test classes on ubuntu/JDK26 alone). The existenceCheckRelabel change (f005) is secondary — even with the old existence-check behavior, the tracking key mismatch would cause failures because the fake repos can't serve the artifacts either way.

Fix PR: apache/maven-resolver#2133 — adds a backward-compatible fallback in applyTracking(): when the nid_hurl lookup misses, tries the system-wide key function (nid) as fallback. If the legacy key matches, the artifact is accepted with a debug log. The entry gets upgraded to nid_hurl on next download. Security intent of f013 is preserved for new entries.

cstamas pushed a commit to apache/maven-resolver that referenced this pull request Sep 8, 2026
…allback (#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)](apache/maven#13078 (comment)).

## 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
@gnodet

gnodet commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Updated the resolver fix PR based on @cstamas's analysis. The first fix only handled legacy nidnid_hurl migration, but the real problem is different:

The ITs override central to file:target/null to prevent remote access. With nid_hurl, the tracking key includes a hash of the repo URL: central-<sha1(https://repo.maven.apache.org/maven2)>central-<sha1(file:target/null)>. Same repo ID, different URL hash → tracking key mismatch → artifact treated as "present but unavailable" → tries to download from file:target/null → 💥

The fix now has a two-stage fallback:

  1. Stage 1: ID-only match (handles legacy nid entries from older resolvers)
  2. Stage 2: repo-ID prefix match (handles same-id-different-URL nid_hurl entries — the IT scenario)

This effectively relaxes f013 to recording-only: entries are written with URL-qualified keys, but lookups fall back to repo-ID matching. All 585 resolver tests pass.

Fix PR: apache/maven-resolver#2133 (force-pushed with the updated fix)

@ascheman

ascheman commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@cstamas — the SNAPSHOT and IT-explosion questions are resolved on my side (thanks, and to @gnodet for #2133). One code point from the validation consolidation itself, independent of the IT fix:

The removed DefaultMetadataReader.validateMetadata() validated both plugin.getArtifactId() and plugin.getPrefix(), but the consolidated ValidatingMetadataXpp3Reader.validate() adds only plugin.getArtifactId()getPrefix() is no longer validated. Since the plugin prefix is carried forward as a coordinate/path component (the old reader's comment flagged prefixes as such), is dropping its validation intentional, or should it get a PathUtils.validatePathComponent(plugin.getPrefix(), "plugin/prefix") alongside the artifactId?

Two minors: validate() went publicprivate (fine — internal package, no external caller left), and there's no test yet exercising the plugin-token validation path.

@slachiewicz
slachiewicz removed their request for review September 9, 2026 14:53

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: validation consolidation looks correct, one gap to fix

The consolidation from two separate validation sites (DefaultMetadataReader.validateMetadata() and DefaultRepositoryMetadataManager.validateVersioning()) into the single ValidatingMetadataXpp3Reader.validate() is clean — it eliminates the duplicate validateVersioning() call and the now-redundant per-reader validation logic.

The new validate() method correctly covers versioning tokens (latest, release, versions[], snapshotVersions[].version, snapshot.timestamp) and plugin.getArtifactId(). The snapshot timestamp validation and metadata.getVersion() validation are new additions not present in either old reader — good.

However, as @ascheman noted: plugin.getPrefix() validation was dropped. The removed DefaultMetadataReader.validateMetadata() explicitly validated both plugin.getArtifactId() and plugin.getPrefix(), with its Javadoc stating these "get carried forward by callers as if they were already-validated path and coordinate components." The consolidated reader only validates plugin.getArtifactId().

Fix: add PathUtils.validatePathComponent(plugin.getPrefix(), "plugin/prefix"); alongside the artifactId validation in ValidatingMetadataXpp3Reader.validate().

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Claude Code on behalf of @gnodet

@cstamas
cstamas marked this pull request as ready for review September 17, 2026 07:27
@cstamas
cstamas requested a review from gnodet September 17, 2026 07:27

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: previous finding addressed, one gap remains

The plugin.getPrefix() validation raised in the previous review is now correctly added alongside plugin.getArtifactId() in ValidatingMetadataXpp3Reader.validate() — that concern is resolved.

The consolidation itself is clean:

  • The double validateVersioning() call in DefaultRepositoryMetadataManager.readMetadata() (once inside the try block and once after it — running twice on the happy path) is correctly removed
  • validate() going private is safe — no external callers exist in the codebase
  • IllegalArgumentExceptionIOException wrapping is consistent and chains the cause correctly
  • Resolver 2.0.23 is released on Central ✓

One gap: the new plugin validation path (plugin/artifactId, plugin/prefix) has no test coverage. The existing tests exercise versioning tokens and snapshot timestamps, but there is no test with a metadata file containing an invalid plugin token (e.g. a plugin.getPrefix() with a / or ..). The validation code is correct, but there's no guard against a regression there.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment on lines +69 to +71
for (Plugin plugin : metadata.getPlugins()) {
PathUtils.validatePathComponent(plugin.getArtifactId(), "plugin/artifactId");
PathUtils.validatePathComponent(plugin.getPrefix(), "plugin/prefix");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Missing test coverage: The plugin validation block added here (lines 69–71) is not exercised by any test. The existing tests in DefaultRepositoryMetadataManagerTest and DefaultRepositoryMetadataManagerValidationTest only cover versioning tokens and snapshot timestamps — neither fires when plugin.getArtifactId() or plugin.getPrefix() contains an invalid character. Add a test resource (e.g. metadata-invalid-plugin-prefix/maven-metadata.xml) with a <plugin> whose prefix contains / or .., and assert the expected exception.

@cstamas cstamas changed the title Resolver 2.0.23-SNAPSHOT and validation cleanup Resolver 2.0.23 and validation cleanup Sep 17, 2026

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: previous finding addressed — LGTM

The missing plugin validation test coverage flagged in the last review is now in place:

  • testMetadataWithInvalidPluginArtifactIdIsRejected() → exercises plugin/artifactId rejection on some/../artifactid (contains /)
  • testMetadataWithInvalidPluginPrefixIsRejected() → exercises plugin/prefix rejection on some/prefix (contains /)

The message substring assertions ("Invalid plugin/artifactId", "Invalid plugin/prefix") correctly match the PathUtils.validatePathComponent error format, which propagates through the IOException wrapping in ValidatingMetadataXpp3Reader.read() and the RepositoryMetadataReadException in DefaultRepositoryMetadataManager.readMetadata().

Rest of the consolidation remains solid: no external callers of validate() (making it private is safe), IllegalArgumentExceptionIOException chaining is correct, and the resolver bump to 2.0.23 is clean.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants