Skip to content

Stop attesting Maven pins a profile or user property overrides - #2005

Open
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
fix/maven-discovery-false-attestation
Open

Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
fix/maven-discovery-false-attestation

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

VEX discovery (vex/discover/maven.rs) attested Maven pins that the build may not resolve. Hosted and vendored modes are ground truth here, so discovery now matches them and Maven's own precedence.

Bug 1: profile declarations (J1)

Discovery left every <profiles> dependency out of the version grouping. It diagnosed only suffixed profile versions. Example:

<dependencies>
  <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId>
    <version>1.7.36-socket.7c8d9e0f</version></dependency>
</dependencies>
<profiles><profile><id>p</id><dependencies>
  <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId>
    <version>1.7.36</version></dependency>
</dependencies></profile></profiles>

Run with -Pp and Maven resolves the public 1.7.36 jar, yet discovery attested the hosted ref. The hosted rewriter leaves profile declarations as they are (redirect_maven_profile_dependency_unpatched). Vendored mode treats every profile as live and rewrites it.

Fix: the ref is kept, so list, rollback and remove still find it, but it is marked unattested in these cases:

  • Main jar, new kind MavenProfileUnpatched (CLI note vex_maven_profile_unpatched): a profile <dependencies> entry for the GA's main jar at any version other than the pin. A profile <dependencyManagement> entry counts only when management governs the pin, meaning there is no live direct <version>. A declaration with no version inherits the live pin. A declaration typed pom, java-source or javadoc carries no code.
  • Executable classifier copies, existing kind MavenClassifierUnpatched: one shared check now covers live and profile copies. A copy with no <version> takes its version from the <dependencyManagement> entry with the same classifier. A profile's entry overrides the live one, and with no entry in this pom the check fails closed.

The pre-v5 single-pom vendored wiring runs the same checks. There, any executable classifier copy counts, because the vendored tree holds only the main jar.

Bug 2: .mvn/maven.config user properties (J4)

A ${prop} <version> was resolved from the pom's <properties> only. Example: the pom has <slf4j.version>1.7.36-socket.7c8d9e0f</slf4j.version> and <version>${slf4j.version}</version>, and .mvn/maven.config has -Dslf4j.version=1.7.36. Maven reads user properties first and builds 1.7.36, yet discovery attested the pin.

Fix: PomDep now carries two versions:

  • version: the version as the pom spells it, which is what hosted mode writes and rolls back.
  • build_version: the version as the build resolves it. -D user properties come first, except project.* / pom.*, which Maven reads from the model.

Pins are found from version. If maven.config moves a pin, the ref stays but is marked unattested with the new kind MavenConfigOverride (CLI note vex_maven_config_override). If only maven.config supplies the Socket version, it is diagnosed (DIAG_REF_UNATTRIBUTABLE, naming .mvn/maven.config) and never becomes a pom.xml ref, so no consumer edits pom.xml for it.

I did not reuse the vendored reactor's interpolator (Reactor::lookup_with): it is a method on the multi-pom Reactor and would need a large refactor. This is the minimal change in formats/maven. The hosted writer still refuses ${…} versions, and that policy is unchanged.

Review findings (/code-review high)

Every behavior finding was reproduced on the first commit before fixing. I ran the extended tests with a temporary probe that printed each case's outcome. All are fixed in the second commit; none were refuted.

  1. Profile <dependencyManagement> flagged beside a direct literal pin; profile classifier management flagged with no declaration taking it. Confirmed: both cases were unattested. Fixed: profile management counts only when the live pin is managed, and classifier management only through a declaration with no <version>. The old test case managed(Some("1.7.36")) beside a direct pin now expects an attested pin. A new case with a managed live pin expects it unattested.
  2. A profile classifier copy with no <version> was always flagged. Confirmed: it was flagged even when the live classifier-keyed management pins it. Fixed by the shared classifier check.
  3. A pin overridden by maven.config stopped being a ref. Confirmed: there was no ref at all. Fixed: the ref is kept and marked MavenConfigOverride. Added the new kind, the CLI note and CLI_CONTRACT.md rows.
  4. A pin supplied only by maven.config was attested as a pom.xml ref. Confirmed. Fixed: it is now a diagnostic naming .mvn/maven.config and is not a ref.
  5. Vendored mode ignored live executable classifier copies. Confirmed. Fixed: one shared check covers hosted and vendored. formats::maven::{NON_EXECUTABLE_CLASSIFIERS, is_executable_classifier} is added with the same text and position as Stop vendored Maven liveness attesting unpatched builds #2006, so the two merge cleanly.
  6. A non-jar <type> profile declaration counted as a main-jar override. Confirmed for pom, which is now ignored, as are java-source and javadoc. Partly refuted for code-carrying types: test-jar or ejb put a jar of the GA on a classpath, so they still withhold the attestation. Ignoring them would attest unpatched code.
  7. -Dproject.version resolved a ${project.version} version. Confirmed: it was attested. Fixed: the build view drops project.* / pom.* user properties.
  8. Duplicated scans. Fixed: main_jar_declarations / classifier_declarations take a scope predicate and back ga_versions, the classifier check and the profile check. group_by_ga is gone.
  9. trusted_checksums re-read .mvn/maven.config. Fixed: it now takes the text extract already read.

Tests

  • New in vex::discover::maven::tests:
    • version_property_reads_maven_config_overrides: override, maven.config-only and project.version cases.
    • profile_declaration_off_the_pin_is_not_attested: main jar, management precedence, classifier and type cases.
    • vendored_ref_bypassed_by_a_profile_or_classifier_is_not_attested.
  • New in formats::maven::tests:
    • version_property_reads_user_properties_first
    • model_reference_version_ignores_user_properties
  • cargo test -p socket-patch-core --lib -- vex::discover:: formats::maven vendor::jvm::: 708 passed.
  • A broader -- maven vex:: filter: 1055 passed.
  • The golden test committed_fixture_corpus_matches_golden passes, and the golden output is unchanged.
  • The CLI vex_sources tests (14) and the contract_gradle_codes test pass.
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check are clean.

Not covered

  • Profile <properties> that redefine a property a live ${…} pin uses are still not read. Vendored mode handles this through version_override. Hosted pins are always literals.
  • Property versions still resolve only one level deep.

Review findings, round 2 (/code-review high on 9765178..2af00b2, cloud routine)

  1. MavenConfigOverride flagged a config property AT the pin: confirmed and fixed. It compared build-resolved versions with the pom-spelled ones. A pre-v5 vendored ${commons.version} that only -Dcommons.version=1.10.0 defines (the vendored version) was therefore reported as an override. It's now one only when the build resolves something other than the pin. The detail lists versions plainly, not as a debug-printed set. Test: vendored_version_from_maven_config_at_the_pin_is_attested. It failed on 2af00b2.
  2. A version-less profile management entry counted as an unpatched classifier copy: confirmed and fixed. It overrides no version, so it's skipped, as profile_override already does for the main jar. Test: a_versionless_profile_management_entry_overrides_nothing. It failed on 2af00b2.
  3. Duplicate config-only diagnostics: fixed. There is now one diagnostic per GA and version.
  4. The rewriter had its own sources/javadoc list: fixed. It calls is_executable_classifier.
  5. Contract wording: fixed. The vex_maven_classifier_unpatched and vex_maven_config_override rows, the UnattestedKind doc and the CLI notes now say pre-v5 vendored refs get these kinds too.

Deferred to landing:

Declined: the extra ga_versions pass, the redundant filter and the tuple shape. These are performance and style only.

Tests (cloud run): cargo test -p socket-patch-core --lib -- formats::maven vex::discover:: patch::redirect::tests::maven gives 483 passed, golden included. CLI --lib vex_sources passes. clippy and fmt are clean.


Note

Medium Risk
Alters Maven attestation and ref discovery logic (compliance-critical path); behavior is narrow but complex—regressions could wrongly attest or omit patches.

Overview
What changes

Maven VEX discovery now aligns with how Maven actually resolves versions: PomDep carries both the pom-spelled version (what hosted rewrite edits) and build_version after applying .mvn/maven.config -D user properties in Maven order (model ${project.*} / ${pom.*} still win over -D).

New / tightened unattested cases (ref stays for list/rollback; vex omits): MavenProfileUnpatched when a <profiles> dependency/management entry moves the pinned GA’s main jar off the pin (with precedence for direct vs management and non-code types ignored); MavenConfigOverride when -D makes the effective GA version differ from the pin spelled in the pom; MavenClassifierUnpatched now also covers classifier copies in profiles and applies consistently to hosted and legacy vendored wiring. Pins supplied only via maven.config are diagnosed as not pom.xml wiring, not attested as refs.

Implementation adds shared helpers (bypasses, profile_override, unpatched_classifier_copy, etc.), reuses the already-read maven.config for trusted checksums, and extends CLI omission notes plus CLI_CONTRACT.md (unattested rules and warning codes).

Reviewed by Cursor Bugbot for commit 2af00b2. Configure here.


Generated by Claude Code

VEX discovery attested two Maven pins the build may not resolve:

1. Profile declarations (J1). Discovery dropped every dependency inside
   <profiles> from the version grouping and diagnosed only suffixed
   profile versions. A profile declaring the pinned GA at the base
   version (or any other version) left the hosted ref attested, yet
   whenever that profile is active Maven resolves the unpatched jar. The
   hosted rewriter leaves profile declarations as written
   (redirect_maven_profile_dependency_unpatched) and vendored mode treats
   every profile as live, so discovery now agrees with the writers: a
   profile declaration of the pinned GA (main jar in <dependencies> or
   <dependencyManagement>, a declaration an undefined coordinate property
   may make that GA, or an executable classifier copy) at any version
   other than the pin keeps the ref but marks it unattested with the new
   UnattestedKind::MavenProfileUnpatched (CLI note
   vex_maven_profile_unpatched). A profile declaration with no <version>
   takes the live pin and does not withhold it. The pre-v5 single-pom
   vendored wiring gets the same check against its vendored version.

2. .mvn/maven.config user properties (J4). A ${prop} <version> was
   resolved from the pom's <properties> only, though coordinates already
   read maven.config's -D user properties. Maven looks user properties up
   first, so <version>${v}</version> with <v>1.0-socket.x</v> in the pom
   and -Dv=1.0 in maven.config builds the base release while discovery
   attested the pin. PomScope now lays the user properties over the root
   <properties> once, and both version resolution and coordinate
   interpolation read that map. The vendored reactor's interpolator is a
   method on the multi-pom Reactor, so it is not reused here; this is the
   minimal change in formats/maven.

Tests (each fails on origin/main):
- formats::maven::tests::version_property_reads_user_properties_first
- vex::discover::maven::tests::version_property_reads_maven_config_overrides
- vex::discover::maven::tests::profile_declaration_off_the_pin_is_not_attested
- vex::discover::maven::tests::vendored_ref_under_a_profile_version_is_not_attested
No existing test asserted the old behavior; the golden corpus is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the profile / user-property attestation fix:

- A profile <dependencyManagement> entry was flagged even when the live
  pin is a direct literal <version>, which beats management; likewise a
  profile-managed classifier entry no declaration takes. Profile
  management now withholds the pin only when management governs it (no
  live direct version). The old test case managed(Some("1.7.36")) beside
  a direct pin asserted the wrong outcome and now expects an attested
  pin; the governing case (managed live pin) expects the opposite.
- A version-less profile classifier copy was always flagged. One shared
  classifier check now covers live and profile copies and resolves a
  version-less copy from the classifier-keyed management (live, with a
  profile's entry overriding it; none in this pom fails closed). Profile
  classifier copies report MavenClassifierUnpatched, like live ones.
- The same check now runs for the pre-v5 vendored wiring too, where any
  executable classifier copy comes from the next repository (the vendored
  tree holds only the main jar). formats::maven gains
  NON_EXECUTABLE_CLASSIFIERS / is_executable_classifier, worded as in
  #2006 so the two merge cleanly.
- A pom-typed (or java-source / javadoc) profile declaration carries no
  code and no longer counts against the pin. Other types (test-jar, ejb)
  still do: they put a jar on a classpath.
- PomDep now keeps both the version as the pom spells it (`version`, what
  hosted writes and rolls back) and as the build resolves it
  (`build_version`, -D user properties first). Pins are found from the
  former, so a pin .mvn/maven.config moves stays a ref (list / rollback /
  remove and liveness keep the wiring) but is marked unattested with the
  new UnattestedKind::MavenConfigOverride (CLI note
  vex_maven_config_override). A Socket version only maven.config
  supplies is diagnosed (DIAG_REF_UNATTRIBUTABLE naming
  .mvn/maven.config), never a pom.xml ref, so nothing edits pom.xml for
  it.
- The build view leaves project.* / pom.* to the model: -Dproject.version
  no longer resolves a ${project.version} dependency version.
- Cleanup: main_jar_declarations / classifier_declarations take a scope
  predicate and back ga_versions, the classifier check and the profile
  check; group_by_ga is gone (live_gas lists the pin candidates);
  trusted_checksums takes the maven.config text extract already read.

Tests: version_property_reads_maven_config_overrides,
profile_declaration_off_the_pin_is_not_attested and
vendored_ref_bypassed_by_a_profile_or_classifier_is_not_attested
(renamed from vendored_ref_under_a_profile_version_is_not_attested) were
extended with a case per finding; each new case failed on the previous
commit. formats::maven gains model_reference_version_ignores_user_properties.
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 4, 2026
Closes the two Maven false-attestation gaps #2005 left open in VEX
discovery.

1. Profile <properties> redefining a pin's property. A live
   <version>${v}</version> that the root <properties> resolve to the
   Socket pin resolves something else whenever an active profile's
   <properties> redefine v (or a property v names): Maven merges active
   profile properties over the pom's own before interpolating. Vendored
   mode refuses to pin over such a property (version_override); discovery
   attested it. PomScope now reads each profile's <properties>, and every
   PomDep carries version_alternatives: the other versions its
   build_version takes under any profile or set of profiles active
   together (user properties and project.* model references stay fixed; a
   value some set leaves unresolved, cyclic or too deep counts as the text
   as written). profile_override now walks live main-jar declarations too
   (same main_jar_declarations scope and management-governs filter): one
   whose build version is the pin but has an alternative off it marks the
   ref unattested with the existing UnattestedKind::MavenProfileUnpatched
   (CLI note and CLI_CONTRACT.md updated), in hosted and the pre-v5
   vendored wiring alike. Profile declarations and the shared classifier
   check (unpatched_classifier_copy) count every alternative as a version
   the declaration may resolve. A <profiles> declaration's own ${v} reads
   its own profile's <properties> first.

2. Property versions resolved one level deep. ${a} with <a>${b}</a> now
   resolves through every property it names, in both views PomDep keeps:
   `version` (as the pom spells it: model, own profile, root properties)
   and `build_version` (.mvn/maven.config user properties first at every
   level; project.* stays the model's). Limits are the vendored reactor's:
   at most 16 levels, and a cycle, undefined name or unterminated ${ never
   resolves (the version stays as written, which is never a pin). A partly
   literal version (${a}.${b}-x) resolves too, and ${project.version}
   resolves from the model in both views.

The vendored reactor's interpolator (interpolate_by and the duplicate
body of Reactor::interpolate_with) moves to formats/maven as the pure
interpolate_strict, built on interpolate_each, which enumerates the
values a name may take (capped at 16 results). Both now detect cycles
explicitly and bound work (1024 lookups, 4096 bytes), so a pom whose
properties each name the next many times over fails closed instead of
expanding exponentially. Reactor::interpolate drops its always-zero
depth argument. Vendored behavior is otherwise unchanged.

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_redefining_a_pin_property_are_not_attested
- vex::discover::maven::tests::profile_declaration_reads_its_own_profile_properties
- vex::discover::maven::tests::version_property_resolves_through_nested_properties
  (16-deep chain resolves, 17 does not; two cycles; nested user overrides)
- vex::discover::maven::tests::vendored_ref_under_profile_properties_is_not_attested
- formats::maven::tests::{version_properties_resolve_recursively_and_fail_closed,
  profile_properties_give_version_alternatives, interpolation_is_bounded}
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

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

Stale Bugbot comment from a previous run.

…classifiers

The "only through .mvn/maven.config" diagnostic fired whenever the build
resolved a different Socket version than the pom spelled, even when the
pom spelling was itself a Socket pin. That pin is pom.xml wiring: pin
detection keeps its ref and reports MavenConfigOverride, so the
diagnostic contradicted it. Skip it when the pom spelling is a pin.

The executable-classifier scan matched only a literal groupId/artifactId,
so a classifier copy declared through an undefined ${property}
coordinate was ignored and the main-jar pin could still be attested
while Maven might put that unpatched classifier jar on the classpath.
Match coordinates with may_name, as main-jar grouping already does.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

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

Stale Bugbot comment from a previous run.

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 6, 2026 12:35
A version-less executable classifier copy took its version from any
managed classifier entry `may_name` matched, including one whose groupId
or artifactId is an undefined property. That entry may manage another
GA, so when it was the only one and sat at the pin, discovery attested
while Maven could resolve the copy from outside this pom, unpatched.
Such entries still count as possible versions, but without a live entry
that names the GA literally the copy's version is treated as unknown.
Addresses Bugbot's classifier-management finding on this PR.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2af00b2. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 6, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 2af00b2b.

  • CI: 29/31 non-skipped checks green on 2af00b2b. The only failures are the main-wide e2e … pdm:: --ignored leg (hishel 2; also red on main; Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes it) and the ci-ok aggregate that follows from it. The full coverage set on the previous head 64a07dae was green apart from that same leg.
  • Bugbot:
    • Fixed in 2af00b2b: "Classifier management over-match attests unpatched copies". A version-less executable classifier copy whose only managed entry reached the GA through an undefined coordinate property inherited that entry's version. That entry may manage another GA, so it no longer decides the version on its own: without a literal live entry the version is treated as unknown (fail closed). A regression row in executable_classifier_copy_off_the_pin_is_not_attested fails without the fix.
    • Bugbot re-reviewed 2af00b2b: no new issues, no unresolved threads.
  • What a reviewer should look at: unpatched_classifier_copy in vex/discover/maven.rs (the literal-vs-property distinction), and the profile / user-property bypass checks from the earlier commits.
  • Slack: not announced. The Slack send action isn't available in this run, so this comment has no slack-announced marker and the next run will retry.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does: Maven VEX discovery stops attesting pins the build may not use. (1) If a <profiles> entry declares the pinned GA's main jar at another version, or an executable classifier copy (live or in a profile) resolves another version, the pin stays a ref but is not attested. (2) ${prop} versions now resolve .mvn/maven.config -D user properties before pom <properties>, as Maven does. A pin that maven.config moves is not attested; a Socket version that only maven.config supplies becomes a diagnostic, not a pom.xml ref.

Risk: medium. This is the attestation path, and it is about 800 lines of rule logic. But every change withholds or narrows an attestation, never adds one, and the golden corpus output is unchanged.

Look here:

Verified:

  • Read the full diff, with an independent second review.
  • cargo test -p socket-patch-core --lib -- vex::discover:: formats::maven vendor::jvm::: 708 passed.
  • The new tests cover each change and would fail without it.
  • maven.config parsing handles -Dk=v, -D k=v, --define, quoting, comments and CRLF.
  • CHANGELOG.md is untouched. Bugbot is clean on 2af00b2. No unresolved threads.
  • CI: 29 passed, 19 skipped. The 2 red (ci-ok and the pdm e2e) are the e2e failure on every PR, which Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes.

Changes I made: none.

Open questions (none block merging):

  1. Profile <properties> that redefine a property a pin is spelled through are still not read. For example, root <slf4j.version>1.7.36-socket.x</slf4j.version> with a profile setting 1.7.36 is still attested. The PR's "Not covered" section says so; hosted mode writes literal versions, and draft Resolve Maven pin properties through profiles and nesting #2020 follows up on it.
  2. MavenConfigOverride also fires for vendored refs, but the docs and CLI note say "hosted Maven pin". In one rare case it withholds a vendored ref the build does use: the pom spells ${v}, the pom doesn't define v, and maven.config sets -Dv to the vendored version. Small follow-up.
  3. The config-only diagnostic prints the version after property resolution, not the ${…} text the pom actually spells.

Approving now doesn't merge it yet: auto-merge is not armed because ci-ok is red from the main-wide pdm failure. I'll arm it once CI is green after #2028 lands.


Generated by Claude Code

Second review round on Maven discovery false attestation.

- `MavenConfigOverride` compared the build-resolved versions with the
  pom-spelled ones, so a property that only `.mvn/maven.config` defines
  AT the pinned version (a pre-v5 vendored `${commons.version}`) was
  reported as an override. It is one only when the build resolves
  something other than the pin; the detail lists versions plainly
  instead of a debug-printed set.
- A version-less profile `<dependencyManagement>` entry for a classifier
  copy (scope or exclusions only) counted as an unpatched copy; it
  overrides no version and is skipped, as `profile_override` already
  does for the main jar.
- The config-only Socket-version diagnostic is emitted once per GA and
  version, not once per declaration.
- The hosted rewriter uses the shared `is_executable_classifier`; the
  contract rows, `UnattestedKind` doc and CLI notes say pre-v5 vendored
  refs get these kinds too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants