Skip to content

Stop vendored Maven liveness attesting unpatched builds - #2006

Draft
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/vendored-maven-liveness
Draft

Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/vendored-maven-liveness

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Vendored JVM (Maven reactor) liveness attested in cases where hosted mode and VEX discovery don't. The owner rule says hosted's stricter behavior is ground truth, so this PR brings vendored mode in line.

What attested wrongly, and the fix

J2: executable classifier and type copies. The project could also declare an executable copy of the patched GA: <classifier>tests</classifier>, a native classifier, or <type>test-jar</type>. The planner only warned (or, for typed copies, skipped silently), so the entry still attested. The classifier-less pin doesn't retarget the copy, and the vendored tree serves only the main jar, so Maven fetches the public release and its code runs.

  • Now a used (non-dependencyManagement) executable copy is uncertain, and so not attestable, unless every pom that reads it resolves the suffixed version. That check uses the same inheritor/profile override check as the main jar.
  • The rule is shared in formats::maven:
    • is_main_jar: untyped or jar, no classifier.
    • is_executable_variant: a type counts as the classifier it implies (test-jar → tests, ejb-client → client, java-source / javadoc → non-executable). Every other type except pom is executable.
    • is_executable_classifier: everything except sources / javadoc. Hosted mode and discovery use it too.
  • Vendored mode is stricter than discovery in one case: a copy inside a <profile> also blocks attestation, because the profile may be active.

J8: unscoped "wired" check. maven_reactor::wired_checked searched every reactor pom's text for the suffixed version. A suffixed version that appeared only in a build plugin's dependencies, a <profile>, an unused property, another artifact's declaration or a classifier copy kept the entry wired.

  • Now "wired" needs a <dependencies> or <dependencyManagement> declaration outside <profiles> that names the main jar at the suffixed version. Doc::declarations already excludes <build> and <reporting>.
  • ${...} is interpolated in the declaring pom and in every pom that inherits it, as Maven interpolates after inheritance.
  • A version-less declaration must also read the pin from the nearest local <dependencyManagement> on its chain. One pinned local root no longer proves an unpinned sibling root.
  • vex, vendor --check and the planner's refuse_unwired use this check.

J15: fail-open on read errors. entry_wired was unwrap_or(true), so an unreadable or unparseable pom made Discovery::vendor_entry_live report a JVM entry as live. It now fails closed.

GC, the takeover classifier and vendor --check

  • scan --prune GC: unchanged on purpose. It uses entry_references, which still answers "keep" on a read error. The old text search stays as maven_reactor::references_checked for the GC only, because the .mvn/maven.config tail repository also serves plugin resolution: a tree a <build> plugin or a profile still names must not be reverted out from under the build.
  • scan's overlap takeover classifier: now uses a tri-state: apply::entry_liveness, Discovery::vendor_entry_liveness and LedgerLiveness::vendor_entry_state. "Unknown" is not "dead": an unreadable pom, or a tree still referenced without attestable wiring, gives None. The classifier stays silent then, instead of emitting redirect_supersedes_vendored ("remove ") for an entry the GC keeps.
  • vendor --check: now agrees with vex. An intact, wired entry that isn't attestable stays verified, and the run gets the new warning vendor_jvm_unattested, which names the declarations. The new code is documented in CLI_CONTRACT.md. It's a warning, not a failure, because of the contract's unattested-references rule: liveness gates keep such wiring live, and re-vendoring cannot clear it.

Other behavior changes

  • vendor's kept-older-patch rule: an unreadable kept entry now means the unserved upgrade fails as vendor_prebuilt_required instead of being a benign skip.
  • refuse_unwired: a plan whose only wiring is a rewritten profile declaration, with no root pin, is refused as nothing_wired.
  • A typed executable copy now adds a classifier_declared degraded warning to the plan.

Review findings (/code-review high)

All nine are confirmed and fixed. Each behavior test fails when its fix is neutralized.

  1. test-jar skipped before the classifier check. Fixed with the shared type rule. plugin_dependencies_exclusions_and_other_types_are_untouched pinned the old silent pass-through; it now expects executable types to warn and not attest, while pom and java-source still pass silently.
  2. Takeover regression on undecided claims. Fixed with the tri-state. Test: maven_takeover_stays_silent_on_an_undecided_vendored_claim (CLI), with a superseded control case.
  3. Classifier copy version never override-checked. Fixed. Test: a_classifier_copy_a_reader_moves_back_to_the_base_is_not_attestable.
  4. One pinned root proved an unpinned sibling root live. Fixed with the version-less management check. Test: a_version_less_declaration_under_an_unpinned_root_is_not_attestable.
  5. vendor --check ignored J2. It now runs unattestable_checked and warns vendor_jvm_unattested. Test: vendor_check_reports_an_unattestable_entry.
  6. Interpolation only in the declaring pom. Fixed by also interpolating in inheritors. Test: a_parent_declaration_its_modules_resolve_to_the_pin_is_wired. attestable_checked still treats a parent declaration whose ${...} the parent itself can't resolve as uncertain (property_unresolved); that rule predates this PR and I left it strict.
  7. Duplicate version interpolation. Now uses Doc::coordinate.
  8. Copy-pasted main-jar predicate. Extracted to Doc::is_main_jar / formats::maven::is_main_jar.
  9. Uncalled maven_reactor::wired. Removed.

Tests

  • Results:
    • cargo test -p socket-patch-core --lib -- vendor::jvm:: vex::discover:: formats::maven: 710 passed
    • golden committed_fixture_corpus_matches_golden: passes, output unchanged
    • -- vendor:: patch::redirect:: utils::purl_key: 3756 passed
    • CLI lib -- takeover commands::vendor::: 76 passed
    • --test vendor_jvm_cli: 54 passed
    • --test maven_sidecar_cli: 28 passed
    • --test e2e_vendor_jvm_build: 35 passed, 6 ignored
    • --test e2e_vendor_maven_build: 4 passed, 1 ignored
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check are clean.

🤖 Generated with Claude Code

Three cases where vendored JVM (Maven reactor) liveness proved an entry
"wired" and let vex attest while the build may still use the unpatched
jar. Hosted mode and VEX discovery already refuse all three.

J2, executable classifier copies: when the project also declares an
executable classifier copy of the patched GA (tests, a native build),
the reactor planner only warned `classifier_declared` and moved on, so
`attestable_checked` passed. The classifier-less pin doesn't retarget the
copy and the vendored tree serves only the main jar, so Maven fetches
the public release and its code runs. A used (non-managed) executable
classifier declaration whose version isn't the suffixed one now marks
the reactor uncertain, so the entry isn't attestable. `sources` and
`javadoc` still only warn. The executable-classifier rule now lives in
`formats::maven::is_executable_classifier`, shared with the hosted
rewriter and discovery.

J8, unscoped "wired" check: `maven_reactor::wired_checked` was a
substring match for the suffixed version over every reactor pom's
comment-masked text. A suffixed version left only in a build plugin's
dependencies, a profile, an unused property, another artifact's or a
classifier copy's declaration kept the entry live. It now requires a
`<dependencies>`/`<dependencyManagement>` declaration outside
`<profiles>` (Doc::declarations already excludes `<build>`/`<reporting>`)
naming the GA's main jar at the suffixed version, literally or through a
property the checkout defines. That covers `vex`/`attestable_checked`,
`vendor --check` (`check_entry`) and the plan's `refuse_unwired`. The
old substring check is kept as `references_checked` and used only by
the `scan --prune` GC (`entry_references`): the maven.config tail also
serves plugin resolution, so a tree a `<build>` plugin or a profile still
names must not be reverted out from under the build.

J15, fail-open on read errors: `entry_wired` was
`entry_wired_checked(..).unwrap_or(true)`, so an unreadable or
unparseable pom made `Discovery::vendor_entry_live` (vendor's kept-patch
rule, scan's takeover classifier and vex's ledger liveness) report a JVM
entry as live. It now fails closed (`false`). The GC doesn't use it: it
asks `entry_references`, which still answers "keep" on a read error, so
nothing is deleted on an unknown answer.

Tests (each fails on origin/main):
- maven_reactor: an_executable_classifier_copy_is_not_attestable
- maven_reactor: a_suffixed_version_outside_the_declarations_is_not_wired
- apply: an_unreadable_pom_is_not_live_but_is_kept (also pins that
  entry_references / vendor_entry_in_use still keep the tree)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 4, 2026
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>
Fixes all nine /code-review high findings on #2006.

1. A typed copy of the GA (`<type>test-jar</type>`, which is the `tests`
   jar, or `ejb`, `ejb-client`, `maven-plugin`, an unknown type) was
   skipped with a bare `continue`, so it still attested. A new shared
   rule, `formats::maven::is_executable_variant(type, classifier)`, maps
   types to the classifier they imply and treats every type except
   `pom`, `java-source` and `javadoc` as executable. The reactor now warns
   `classifier_declared` for such a copy and marks it uncertain unless
   every reader resolves it to the suffixed version.
2. With `entry_wired` failing closed, `scan`'s overlap takeover
   classifier read an unreadable pom, or vendored wiring left only in a
   plugin or profile, as a dead vendored claim. It then emitted
   `redirect_supersedes_vendored` ("remove <purl>") while the GC still
   kept the entry. New tri-state: `apply::entry_liveness`,
   `Discovery::vendor_entry_liveness` and `LedgerLiveness::
   vendor_entry_state`. It returns `None` when the claim is undecided
   (read error, or still referenced without attestable wiring), and
   the classifier stays silent on `None`.
3. A classifier copy's version now goes through the same override check
   as the main jar's (`version_moved`, generalized from
   `version_override`). An inheriting pom or a profile that moves the
   property off the suffixed version makes it uncertain.
4. A version-less declaration now reads its version from the nearest
   local `<dependencyManagement>` on its chain, interpolated as the
   declaring pom reads it. Anything other than the suffixed version,
   including no local management (a local root left unpinned), is
   uncertain. One pinned root no longer proves an unpinned sibling root.
5. `vendor --check` now agrees with `vex`. `check_entry` returns
   `Ok(Some(why))` when the entry is intact and wired but not attestable.
   `vendor --check` keeps the entry `verified` and adds the new run
   warning `vendor_jvm_unattested` naming the declarations (documented
   in CLI_CONTRACT.md). It is a warning rather than a failure because
   the contract's unattested-references rule says liveness gates keep
   such wiring live, and re-vendoring cannot clear it.
6. `Reactor::wires` now interpolates each declaration in the declaring
   pom and in every pom that inherits it, as Maven interpolates after
   inheritance. A parent's managed `${ct.version}` that each module
   defines as the pin now counts as wired again, so `refuse_unwired` no
   longer refuses that plan.
7. The classifier version check uses `Doc::coordinate`.
8. The main-jar predicate is `formats::maven::is_main_jar`, used through
   `Doc::is_main_jar` by `wires`, `rewrite_declarations`,
   `warn_unresolved_coordinates` and `jar_declaration`.
9. Removed the uncalled `maven_reactor::wired`.

Tests (each fails with its fix neutralized):
- plugin_dependencies_exclusions_and_other_types_are_untouched: updated.
  It pinned the silent `test-jar` pass-through; executable types now warn
  and don't attest, while `pom` and `java-source` still pass silently.
- a_classifier_copy_a_reader_moves_back_to_the_base_is_not_attestable
- a_version_less_declaration_under_an_unpinned_root_is_not_attestable
- a_parent_declaration_its_modules_resolve_to_the_pin_is_wired
- vendor_check_reports_an_unattestable_entry
- maven_takeover_stays_silent_on_an_undecided_vendored_claim (CLI)
- an_unreadable_pom_is_not_live_but_is_kept also covers the tri-state

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant