Skip to content

RRF: self-heal from provably broken auto-discovered prefixes files - #1976

Merged
ascheman merged 3 commits into
apache:masterfrom
aschemaven:rrf-verify-denied-prefixes
Jul 18, 2026
Merged

RRF: self-heal from provably broken auto-discovered prefixes files#1976
ascheman merged 3 commits into
apache:masterfrom
aschemaven:rrf-verify-denied-prefixes

Conversation

@ascheman

Copy link
Copy Markdown
Contributor

apache/maven#11856 documented the "broken MRM" failure mode: a group/virtual repository leaks a member repository's .meta/prefixes.txt, Resolver trusts the well-formed file, and everything the repository actually serves is denied. It was closed with a doc remark (#1820) and a better error message (apache/maven#11888) — but a single wrong file still silently takes down a whole repository.

We found a large public instance: https://repo.jenkins-ci.org/public/.meta/prefixes.txt (Artifactory virtual repo) serves a leaked 11-prefix file of a long-gone member repository ("Generated by Sonatype Nexus", Last-Modified: 12 Jan 2017). Neither /org/jenkins-ci nor /io/jenkins is listed, so with Resolver 2.x defaults every Jenkins-ecosystem build fails — verified on Maven 4.0.0-rc-5, current 4.0.x snapshots, and Maven 3.10.0-rc-1 — while 3.9.x works. Reported to jenkins-infra: jenkins-infra/helpdesk#5231. Syntactic validation cannot catch this, and the 2.0.14 guards (useMirroredRepositories/useRepositoryManagers) never fire because nothing marks a POM-declared repository as MRM-backed.

Change: when an auto-discovered prefixes file denies a path, the first denial per repository is verified against the repository itself via peek() (transporter sits below the filtering connector — no recursion). Path exists → file is provably wrong: WARN + drop for the session (behaves as noInputOutcome). Path absent / transport error → file stays trusted, request stays denied, no further checks. Bounded cost: at most one existence check per repository per session, only on the deny path — the round trips RRF saves are preserved. User-provided prefix files are never second-guessed; skipped offline; opt-out aether.remoteRepositoryFilter.prefixes.verifyDenied (repoId suffix supported).

First commit is the UT reproducing the Jenkins failure (red without the fix); second commit is the fix + docs. Note for Maven core: the PrefixesRemoteRepositoryFilterSource constructor gained a TransporterProvider parameter — Maven's own wiring needs the same one-liner when upgrading resolver.

Refs apache/maven#11856

🤖 Generated with Claude Code

@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.

Clean, well-bounded fix for a painful real-world problem. The design is sound: verify once per repository per session, only on the deny path, only for auto-discovered (not user-provided) prefixes. The bounded cost (at most one HEAD per repo per session) preserves the round-trip savings RRF provides in the normal case.

Thread safety — Verified: the volatile prefixTree field + AtomicBoolean CAS + synchronized(cachedPrefixes) block correctly ensures exactly one verification per repository, with concurrent denied-path threads waiting for the verdict rather than returning stale denials. The synchronized block around the network peek() is the right trade-off — without it, concurrent threads could observe the original (broken) tree and return false denials while verification is in flight. Since this happens at most once per repo per session, the lock duration is acceptable.

API compatibility — The constructor gained a TransporterProvider parameter. Since the class is in internal.impl and uses @Inject, DI consumers (the primary path) are unaffected. Both RepositorySystemSupplier variants (mvn3/mvn4) are updated. The PR correctly notes Maven core wiring needs the same update when upgrading resolver. ✓

Tests — Thorough coverage of all cases: core scenario (broken prefixes dropped), repository-wide propagation, bounded peek cost, accepted-path passthrough, disabled config, offline mode, and user-provided authority. Using the actual leaked Jenkins prefixes file as test data ties the tests directly to the reported incident.

One suggestion: consider adding a test for the noInputOutcome=false interaction. When broken prefixes are dropped, noInputResult() delegates to the noInputOutcome config (default true/accept). If a user has explicitly set noInputOutcome=false (block when no input), the broken file is detected and dropped, but the path is still denied — the build still fails, just with a different diagnostic. This is arguably correct behavior (the user asked for strict filtering), but a test documenting this interaction would prevent future confusion:

@Test
void brokenPrefixesDroppedButNoInputOutcomeFalseStillDenies() throws Exception {
    session.setConfigProperty(
            PrefixesRemoteRepositoryFilterSource.CONFIG_PROP_NO_INPUT_OUTCOME, "false");
    RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
    assertNotNull(filter);

    // prefixes are broken and dropped, but noInputOutcome=false means denial
    assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
    // verification still happened (peek was called)
    verify(transporter, times(1)).peek(any(PeekTask.class));
}

CI — Matrix still pending (just opened). One pre-existing flaky test (JettyTransporterTest.testGet_HTTP3 on Windows) is unrelated.

Reviewed with oss-code-reviewer on behalf of @gnodet. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

gnodet added a commit to gnodet/maven-resolver that referenced this pull request Jul 18, 2026
gnodet added a commit to gnodet/maven-resolver that referenced this pull request Jul 18, 2026
@ascheman

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Added the suggested noInputOutcome=false interaction test in 07a0c6c — it passes as you predicted: the broken file is still detected and dropped (exactly one peek), but strict filtering keeps the path denied.

Re CI: the matrix failures are unrelated to this PR — master went red at dbaf309 (#1970) with the same UrlTransporterTest/awaitility signature (fixes in flight: #1977/#1978). Will rebase once one of those lands.

@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.

Thanks for adding the noInputOutcome=false interaction test — it looks correct: verification still happens (one peek), the broken file is detected and dropped, but strict filtering honors the user's explicit config. Clean commit.

Re CI: noted that master is red from #1970's awaitility changes — this PR's test-only commit 3 is unaffected. Will re-check once #1977/#1978 lands and you rebase.

Reviewed with oss-code-reviewer on behalf of @gnodet. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

gnodet added a commit to gnodet/maven-resolver that referenced this pull request Jul 18, 2026
gnodet added a commit to gnodet/maven-resolver that referenced this pull request Jul 18, 2026
@cstamas

cstamas commented Jul 18, 2026

Copy link
Copy Markdown
Member

LGTM and fixes landed

Gerd Aschemann and others added 3 commits July 18, 2026 12:53
Replays https://repo.jenkins-ci.org/public/.meta/prefixes.txt serving a
leaked member-repo prefixes file (apache/maven#11856): well-formed, but
denies everything the repository actually hosts. Red without the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verify the first denied path per repository against the repository
itself (one peek, bounded); if it exists, the auto-discovered prefixes
file is provably wrong: warn and ignore it for the session. User-provided
files stay authoritative. Opt-out:
-Daether.remoteRepositoryFilter.prefixes.verifyDenied=false

Refs apache/maven#11856

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documents the interaction suggested in review: a provably broken
auto-discovered prefixes file is still detected and dropped (one peek),
but noInputOutcome=false means the user asked for strict filtering, so
the path stays denied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ascheman
ascheman force-pushed the rrf-verify-denied-prefixes branch from 07a0c6c to d794e05 Compare July 18, 2026 10:54
@cstamas cstamas added the enhancement New feature or request label Jul 18, 2026
@cstamas cstamas added this to the 2.0.21 milestone Jul 18, 2026

In this case, user should disable prefix discovery by using `-Daether.remoteRepositoryFilter.prefixes.resolvePrefixFiles=false` user property to prevent Maven attempting to resolve prefixes file from such broken MRMs.

Since 2.0.21, Resolver protects itself against this failure mode: when an auto-discovered prefixes file denies a path, the very first denial per remote repository is verified against the remote repository itself using a lightweight existence check. If the repository actually serves the denied path, the auto-discovered prefixes file is provably wrong; a warning naming the repository is emitted (report it to the repository administrator) and the file is ignored for the rest of the session, so the build proceeds as if no prefixes file was published. If the denied path is indeed absent remotely, the file remains trusted and no further checks are performed, keeping the extra cost bounded at one existence check per remote repository per session. User-provided prefix files are considered deliberate and are never second-guessed. The verification can be disabled with `-Daether.remoteRepositoryFilter.prefixes.verifyDenied=false` (supports repository ID suffix as well).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

provably -> probably

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional, not a typo: at that point the existence check has just proven the file wrong — it denies a path the repository demonstrably serves (that's the whole trigger condition, we never drop on suspicion). Happy to reword to "demonstrably wrong" if that reads less like a typo.

@ascheman
ascheman merged commit f4d1fa9 into apache:master Jul 18, 2026
20 checks passed
@ascheman
ascheman deleted the rrf-verify-denied-prefixes branch July 18, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants