Skip to content

Vendored Maven reactor pins over an imported BOM or external parent, so a build that uses 1.11.0 is silently downgraded to 1.10.0-socket.* and a later BOM bump never takes effect #488

Description

[agent] Found by the scheduled Maven bug-hunt routine (ledger #318).

Summary

The v5 vendored Maven reactor planner (vendor/jvm/maven_reactor.rs) only checks for version conflicts in declarations it can see in the checkout. A version that comes from an imported BOM (<scope>import</scope>) or from a parent outside the checkout (<relativePath/>, e.g. a corp parent or spring-boot-starter-parent) is never consulted. When a module declares the patched GA without a <version>, vendor adds a <dependencyManagement> pin to <base>-socket.<hex8> in the local root. Local management beats both imported and inherited-external management, so:

  1. Straight downgrade. The BOM (or external parent) manages commons-text:1.11.0 and the build resolves 1.11.0. vendor with a 1.10.0 patch pins 1.10.0-socket.1d3c1fd2, and the build now uses the older 1.10.0 code base. Exit 0, status: success, applied: 1, no warning about the conflict, and vex attests not_affected.
  2. Upgrade blocked. Vendor while the BOM still manages 1.10.0 (correct at that point). Later the team bumps the BOM to one managing 1.11.0. The committed root pin keeps overriding it. Re-running vendor reports already_vendored ("artifact and lockfile wiring already in sync") and changes nothing. The upgrade silently never reaches the build, and VEX keeps attesting.

The planner already handles the local-literal version of this case (conflicting_literal_version, root left unpinned). The BOM and external-parent paths get no such check.

Impact

Running vendor silently changes which library version the build ships: it moves the build to an older line than the one the project's dependency management selects, and from then on it blocks upgrades made through the BOM or parent. Most enterprise Maven projects take versions from a corp BOM or Spring Boot's parent, so this is a common setup. Nothing in the vendor envelope, the re-run, or vex mentions the override. That conflicts with docs/design/maven-vendoring.md ("conflicting explicit versions … produce specific warnings; the backend does not silently claim those unsupported declarations are patched").

Repro

Fixture (reactor, BOM import; the BOM is installed into the local repo first, standing in for a corp BOM):

mkdir -p bom2 f3/a
cat > bom2/pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion>
<groupId>com.corp</groupId><artifactId>corp-bom</artifactId><version>2</version><packaging>pom</packaging>
<dependencyManagement><dependencies>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.11.0</version></dependency>
</dependencies></dependencyManagement></project>
EOF
(cd bom2 && mvn -q install)
cat > f3/pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId><artifactId>root</artifactId><version>1.0.0</version>
  <packaging>pom</packaging>
  <modules><module>a</module></modules>
  <dependencyManagement><dependencies>
    <dependency><groupId>com.corp</groupId><artifactId>corp-bom</artifactId><version>2</version>
      <type>pom</type><scope>import</scope></dependency>
  </dependencies></dependencyManagement>
</project>
EOF
cat > f3/a/pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example</groupId><artifactId>root</artifactId><version>1.0.0</version></parent>
  <artifactId>a</artifactId>
  <dependencies>
    <dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId></dependency>
  </dependencies>
</project>
EOF
cd f3
mvn -q package dependency:build-classpath -Dmdep.outputFile=target/cp.txt; tr ':' '\n' < a/target/cp.txt | grep commons-text
#   …/commons-text/1.11.0/commons-text-1.11.0.jar
# stage a commons-text@1.10.0 patch (.socket/manifest.json + blob), then:
socket-patch vendor --json
mvn -q package dependency:build-classpath -Dmdep.outputFile=target/cp.txt; tr ':' '\n' < a/target/cp.txt | grep commons-text
#   …/.socket/vendor/maven2/org/apache/commons/commons-text/1.10.0-socket.1d3c1fd2/commons-text-1.10.0-socket.1d3c1fd2.jar
socket-patch vex --output vex.json   # exit 0, GHSA-… not_affected

The patch service and manifest were staged with the repo's own harness: a local copy of e2e_vendor_jvm_build::maven_reactor (its warm_fixture, stage_manifest, prebuilt_common::prepare_command), with only the fixture POMs swapped. The fresh-checkout build ran -o with 1.10.0 purged from the local repo, as the capstone does.

Variants, same result:

  • External parent: the root's <parent> is com.corp:corp-parent:2 with <relativePath/> (installed in m2, managing 1.11.0) instead of the BOM import. The build is downgraded to 1.10.0-socket.1d3c1fd2.
  • Upgrade blocked: the root imports corp-bom:1 (managing 1.10.0). Run vendor (correct), then change the import to corp-bom:2 (1.11.0) and run vendor again. The second run returns already_vendored, the pom is unchanged, and the fresh build still resolves 1.10.0-socket.1d3c1fd2.

Expected vs actual

  • Expected (docs/design/maven-vendoring.md, Maven section): a declaration whose effective version is not the patch base is a conflicting version. It gets a specific warning, and the root is not pinned, as conflicting_literal_version already does for local literals. At minimum, an import-scope BOM or a non-local parent that could manage the GA should make the planner warn rather than pin blindly. The same goes for the re-run after a BOM bump.
  • Actual: the pin is written unconditionally. Exit 0, applied: 1, no conflict warning, VEX not_affected, and the re-run reports "in sync".

Matrix (Linux, JDK 21, main c7af4df)

Maven BOM import 1.11.0 external parent 1.11.0 BOM bump after vendoring
3.6.3 blocked (Central 429 in warm-up) blocked untested
3.8.8 blocked (Central 429 in warm-up) blocked untested
3.9.11 downgraded (2/2 runs) downgraded pin kept, re-run "in sync"
4.0.0-rc-7 downgraded downgraded pin kept, re-run "in sync"

This is planner logic with no OS dependence, so there's no macOS/Windows probe. Single-POM projects (no <modules>) don't downgrade: that backend adds only a repository, and the build keeps 1.11.0.

Suspect code

  • crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs:182: None if pinned => pin_edit(…) pins every wired root. pinned is only cleared by rewrite_declarations (:962), which iterates local keyed_declarations and never looks at <scope>import</scope> entries or a parent resolved outside the checkout. metadata_model (:624) already collects imports, but only for Gradle.

Related, but a different root cause: #459 (active-profile <properties>), #265 (hosted crawler picks up versions the project doesn't use).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions