[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).
[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 --checkexits 1 withvendor_check_failed: "vendored wiring or metadata drifted: c/pom.xml". This is correct.vexexits 0 withstatus: success, emitsverified/not_affectedforpkg:maven/org.apache.commons/commons-text@1.10.0on the aggregator product, and writes an OpenVEX statement saying "Patched via Socket patch … (vendored)". Its only warning isvendored_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 modulec's classpath at all.Re-running
vendorfixes the tree: it rewritesc/pom.xmland 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 thatvendor --checkalready does (apply::check_entry).Impact
A VEX document can be produced, for example in CI with
vex --output, that saysnot_affectedfor 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 --checkfails), butvexdoesn'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, uuid1d3c1fd2-…). The fixture is the stock reactor (aggregator →corp-parent,adeclares 1.10.0 literally,bdepends ona). Modulebuses the file-form<relativePath>../corp-parent/pom.xml</relativePath>so that #534 doesn't get in the way.Expected vs actual
vexmust not attestnot_affectedwhile 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 thatvendor --checkruns reports drift,vexshould omit the entry (or fail it), as it does forvendor_unwired/vendor_jvm_shape_unsupported, and point at re-runningvendor.vexexit 0 andnot_affected, whilevendor --checkon the same tree exits 1 and the build ships Central's unpatched jar in modulec.Matrix (Linux, JDK 21, main
bf0e0d1)vendor --checkvexcclasspathvendorThis 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 isreactor.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 onentry_wired_checked(that predicate). The drift check incrates/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'sproperty_unresolvedbranch won't cover this case, because here the planner is never re-run. #534 (vex/--checkrefuse a directory-formrelativePath).