Skip to content

Vendored Maven reactor vex attests not_affected after a module added later declares the base version, although vendor --check flags the drift and that module builds Central's unpatched jar #584

Description

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

Summary

After a reactor is vendored, a developer adds a module (or a dependency in an existing module) that declares the patched GA at its literal base version, here commons-text <version>1.10.0</version>. Maven applies that explicit version over the corp parent's <dependencyManagement> pin, so the new module builds Central's unpatched 1.10.0 jar. socket-patch's two verification commands then disagree on the same tree:

  • vendor --check exits 1 with vendor_check_failed: "vendored wiring or metadata drifted: c/pom.xml". This is correct.
  • vex exits 0 with status: success, emits verified / not_affected for pkg:maven/org.apache.commons/commons-text@1.10.0 on the aggregator product, and writes an OpenVEX statement saying "Patched via Socket patch … (vendored)". Its only warning is vendored_tree_out_of_sync ("the installed tree does not match its vendored artifact … re-run your package manager's install"). That warning is misleading here: reinstalling doesn't help, and the vendored jar isn't on module c's classpath at all.

Re-running vendor fixes the tree: it rewrites c/pom.xml and every module then resolves the patched jar. So the planner is fine. The bug is that VEX liveness for a Maven reactor is "some pom in scope still contains the suffixed version" (maven_reactor::wired_checked), instead of the re-plan that vendor --check already does (apply::check_entry).

Impact

A VEX document can be produced, for example in CI with vex --output, that says not_affected for a vulnerability one reactor module still ships. Adding a module, or adding a direct dependency with a literal version, after vendoring is ordinary day-to-day work. The tool already knows the tree has drifted (vendor --check fails), but vex doesn't use that knowledge. The bug-hunt bar and CLI_CONTRACT's VEX section both treat attesting a patch that the build doesn't consume as a defect (compare #325 / #516 for npm, where VEX attests while one copy is unpatched).

Repro

I used the repo's own harness: a local, uncommitted copy of e2e_vendor_jvm_build::maven_reactor (warm_fixture, stage_manifest, prebuilt_common::prepare_command, the commons-text 1.10.0 NOTICE marker patch, uuid 1d3c1fd2-…). The fixture is the stock reactor (aggregator → corp-parent, a declares 1.10.0 literally, b depends on a). Module b uses the file-form <relativePath>../corp-parent/pom.xml</relativePath> so that #534 doesn't get in the way.

socket-patch vendor --json --offline            # exit 0, applied 1: a/pom.xml -> 1.10.0-socket.1d3c1fd2, corp-parent pin + repo

# developer adds a module afterwards
mkdir c && cat > c/pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId><artifactId>corp-parent</artifactId><version>1.0.0</version>
    <relativePath>../corp-parent/pom.xml</relativePath>
  </parent>
  <artifactId>c</artifactId>
  <dependencies>
    <dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.10.0</version></dependency>
  </dependencies>
</project>
EOF
# + <module>c</module> in the aggregator pom.xml

socket-patch vendor --check --json --offline
#   exit 1, events[0] {action: failed, errorCode: vendor_check_failed,
#                      reason: "vendored wiring or metadata drifted: c/pom.xml"}
socket-patch vex --json --output vex.json
#   exit 0, status success, events[0] {action: verified, status: not_affected}
#   warning vendored_tree_out_of_sync; vex.json statement: not_affected,
#   "Patched via Socket patch 1d3c1fd2-7b4e-4c1a-9f0e-2a3b4c5d6e7f (vendored)"

# fresh checkout, commons-text 1.10.0 + suffixed purged from the local repo
mvn package org.apache.maven.plugins:maven-dependency-plugin:3.5.0:build-classpath -Dmdep.outputFile=target/cp.txt
#   a/target/cp.txt -> .socket/vendor/maven2/…/commons-text-1.10.0-socket.1d3c1fd2.jar   (patched)
#   b/target/cp.txt -> .socket/vendor/maven2/…/commons-text-1.10.0-socket.1d3c1fd2.jar   (patched)
#   c/target/cp.txt -> m2/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar (Central, UNPATCHED)

socket-patch vendor --json --offline            # re-run: c/pom.xml rewritten; a, b, c all patched afterwards

Expected vs actual

  • Expected: vex must not attest not_affected while a reactor module builds the unpatched artifact. CLI_CONTRACT ("Liveness gates") says a vendor ledger entry attests only while the config still wires its artifact, and docs/design/maven-vendoring.md says the backend "does not silently claim those unsupported declarations are patched". At minimum, when the same check that vendor --check runs reports drift, vex should omit the entry (or fail it), as it does for vendor_unwired / vendor_jvm_shape_unsupported, and point at re-running vendor.
  • Actual: vex exit 0 and not_affected, while vendor --check on the same tree exits 1 and the build ships Central's unpatched jar in module c.

Matrix (Linux, JDK 21, main bf0e0d1)

Maven vendor --check vex module c classpath after re-running vendor
3.8.8 fail (drift, correct) not_affected Central 1.10.0 (unpatched) patched
3.9.16 fail (drift, correct) not_affected (2/2) Central 1.10.0 (unpatched) (2/2) patched
4.0.0-rc-7 fail (drift, correct) not_affected Central 1.10.0 (unpatched) patched

This is VEX liveness logic with no OS dependence, so I ran no macOS/Windows probe. Not bisected: the reactor backend is new in v5 (#277).

Suspect code

  • crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs:387 (wired_checked): liveness is reactor.scope.iter().any(|rel| masked.contains(&sv)), so one pinned pom makes the whole reactor "wired".
  • crates/socket-patch-cli/src/commands/vex_sources.rs:347: JVM entries are gated only on entry_wired_checked (that predicate). The drift check in crates/socket-patch-core/src/vendor/jvm/apply.rs:707 (check_entry, which re-plans and fails on any pending write) isn't consulted.

Related, but with different triggers: #513 (the planner leaves an unresolved ${prop} declaration and VEX attests; its suspect list also names this liveness predicate). Fixing #513's property_unresolved branch won't cover this case, because here the planner is never re-run. #534 (vex / --check refuse a directory-form relativePath).

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