Repository navigation
Vendored Gradle silently downgrades a version-range dependency to an older unpatched release (1.10.0 → 1.9), because the vendored repository has no maven-metadata.xml; vendor --check and VEX still report it patched #511
Description
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
When a Gradle build declares the patched library with a version range ([1.9,1.10.0], or a rich strictly '[1.9,1.11)'; prefer '1.10.0'), it resolves the patched base version (1.10.0) before vendoring. After socket-patch vendor the same build resolves commons-text 1.9 from Maven Central instead: an older release that isn't patched.
The cause is the generated exclusiveContent { filter { includeVersion(g, a, "1.10.0") } }. It removes 1.10.0 from Central's candidate list. The socketPatchVendor file repository, .socket/vendor/gradle/<g>/<a>/, has no maven-metadata.xml, so it lists no versions for the range. Gradle then picks the highest version that's left, 1.9.
Nothing reports the downgrade:
vendorexits 0 with no warning.vendor --checkreturnsvendor_check_ok("committed artifact and wiring verified").vexemitsnot_affected/inline_mitigations_already_existforpkg:maven/org.apache.commons/commons-text@1.10.0, a version the build no longer uses.
Impact
A project that pins a range, or a narrow strictly range with a prefer, ends up on a different and older version after vendoring, with no error. The patch is never used, and the build may now carry vulnerabilities that 1.10.0 had already fixed. The VEX attestation is false.
Repro (Linux, Gradle 8.14.3 and 9.8.0, JDK 21, main 61cfb9b)
mkdir p && cd p
cat > settings.gradle <<'EOF'
rootProject.name = 'p'
EOF
cat > build.gradle <<'EOF'
plugins { id 'java' }
repositories { mavenCentral() }
dependencies { implementation 'org.apache.commons:commons-text:[1.9,1.10.0]' }
tasks.register('printCp') { def cp = configurations.runtimeClasspath; doLast { cp.files.each { println "CP " + it } } }
EOF
gradle -q dependencies --configuration runtimeClasspath | grep commons-text
# \--- org.apache.commons:commons-text:[1.9,1.10.0] -> 1.10.0
# stage a patch for pkg:maven/org.apache.commons/commons-text@1.10.0 in .socket/manifest.json + blob
# (the bug-hunt used prebuilt_common::prepare_command, the same as e2e_vendor_jvm_build.rs)
socket-patch vendor --json --offline # exit 0, applied 1, no warnings
rm -rf .gradle build
gradle -q dependencies --configuration runtimeClasspath | grep commons-text
# \--- org.apache.commons:commons-text:[1.9,1.10.0] -> 1.9 <-- downgraded, unpatched
gradle -q printCp | grep commons-text
# CP ~/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-text/1.9/.../commons-text-1.9.jar
socket-patch vendor --check --json # exit 0, vendor_check_ok
socket-patch vex --product pkg:maven/x/p@1 # status not_affected for commons-text@1.10.0Confirming the cause: if I hand-write .socket/vendor/gradle/org/apache/commons/commons-text/maven-metadata.xml listing <version>1.10.0</version>, the same build resolves [1.9,1.10.0] -> 1.10.0 from .socket/vendor/gradle/.../commons-text-1.10.0.jar, and that jar has the patch marker.
Expected vs actual
- Expected: vendoring never changes which version Gradle selects.
docs/design/maven-vendoring.mdsays the Gradle backend keeps the original GAV and serves it from the committed repository. For Maven it also says: "Range selectors, unresolved properties, … produce specific warnings; the backend does not silently claim those unsupported declarations are patched." The same section lists "dynamic-version repository metadata" as remaining scope. So either the vendored repo should carry amaven-metadata.xmlfor each vendored module, which fixes it in my test, orvendorshould warn or refuse. In neither case should--checkand VEX report the patch as in effect. - Actual: exit 0, no warning, the build is silently downgraded to an unpatched release,
vendor --checkpasses, and VEX saysnot_affected.
Matrix
| Declaration | Gradle 8.14.3 | Gradle 9.8.0 |
|---|---|---|
[1.9,1.10.0] |
fail (→ 1.9) | fail (→ 1.9) |
{ strictly '[1.9,1.11)'; prefer '1.10.0' } |
fail (→ 1.9) | fail (→ 1.9) |
1.10.0!! (strict exact) |
pass (vendored, patched) | n/t |
1.+ / latest.release |
unaffected (resolves 1.15.0 before and after, so the patch doesn't apply) | n/t |
Linux only. I didn't run macOS or Windows, or Gradle 6.9.4 / 7.6.6. Gradle's version selection doesn't depend on the OS, and the script is the same on every Gradle version ≥ 6.8, so I expect the same result there.
Suspect code
crates/socket-patch-core/src/vendor/jvm/socket-patch.settings.gradle:51:includeVersion(...)insideexclusiveContentexcludes the base version from every other repository.crates/socket-patch-core/src/vendor/jvm/gradle.rs:68(plan): it writes the version directory but no artifact-levelmaven-metadata.xml, and it doesn't scan build scripts for range or rich-version declarations, so nothing warns.vendor --checkandvexcheck only the committed bytes and the wiring, not which version is selected.
Activity
gradle: run 6 ledger (#511)
mikolalysenko commentedon Oct 2, 2026
[agent] Triaged as
priority:p3(Gradle). Not a duplicate, and there's no fix PR. I confirmed on main61cfb9b:vendor/jvm/gradle.rswrites no artifact-levelmaven-metadata.xmlinto.socket/vendor/gradle/<g>/<a>/, and the settings script'sexclusiveContent { includeVersion(...) }removes the base version from every other repository, so a range selector can't see it. Related to #347 and #461 (other gaps in exclusiveContent), but the cause here is different.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Two more data points for this issue, on main
61cfb9bwith Gradle 8.14.3 on Linux:libs.versions.tomlset totext = { require = "[1.9,1.10.0]" }andimplementation libs.text, a fresh clone resolvescommons-text-1.10.0.jarbefore vendoring andcommons-text-1.9.jar(unpatched, from the Gradle cache) aftervendorexits 0. I saw it twice. The vendored 1.10.0 jar does carry the patch marker; it just isn't selected.gradle.lockfilemasks it. WithdependencyLocking { lockAllConfigurations() }and the lockfile written before vendoring (commons-text:1.10.0=…runtimeClasspath…), the same range resolves the vendored.socket/vendor/gradle/…/commons-text-1.10.0.jarafter vendoring. So only unlocked range or catalog declarations are affected.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Claiming this issue as part of the full Gradle support campaign (agent / hosted / vendored modes, tested on Gradle 6.9/7.6/8.x/9.x on Linux, macOS and Windows). Branch: feat/gradle-support. Claim-ID: 2026-10-02T13:56:32Z-gradle21
Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Another data point: the range doesn't have to be in the user's build. A transitive range from a third-party POM causes the same downgrade, and the user can't avoid it by editing their own declarations.
Main
045d7ec, Gradle 8.14.3, JDK 21, Linux. Reproduced 2 out of 2 times, from a clean.gradle.com.example:lib:1.0is in a file repository, and its POM declaresorg.apache.commons:commons-text:[1.9,1.10.0]. The project only declaresimplementation 'com.example:lib:1.0'withrepositories { maven { url = corp }; mavenCentral() }.commons-text:[1.9,1.10.0] -> 1.10.0.vendor --json --offlineforpkg:maven/org.apache.commons/commons-text@1.10.0exits 0 with no warning. The planner can't see the range, because it's in a dependency's POM.commons-text:[1.9,1.10.0] -> 1.9, from~/.gradle/caches/modules-2/.../commons-text-1.9.jar(unpatched).vendor --checkexits 0.vex --product …emitsnot_affectedfor commons-text@1.10.0.So a static warning on range declarations in the project's own build files wouldn't cover this case. A fix probably needs the vendored repository to list the version (an artifact-level
maven-metadata.xml), as the issue body suggests.Generated by Claude Code
Full Gradle support in agent, hosted and vendored modes (#646)
sbt, Mill and scala-cli support in agent, hosted and vendored modes (#…