Skip to content

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

Closed

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:

  • vendor exits 0 with no warning.
  • vendor --check returns vendor_check_ok ("committed artifact and wiring verified").
  • vex emits not_affected / inline_mitigations_already_exist for pkg: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.0

Confirming 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.md says 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 a maven-metadata.xml for each vendored module, which fixes it in my test, or vendor should warn or refuse. In neither case should --check and VEX report the patch as in effect.
  • Actual: exit 0, no warning, the build is silently downgraded to an unpatched release, vendor --check passes, and VEX says not_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(...) inside exclusiveContent excludes 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-level maven-metadata.xml, and it doesn't scan build scripts for range or rich-version declarations, so nothing warns.
  • vendor --check and vex check only the committed bytes and the wiring, not which version is selected.

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p3 (Gradle). Not a duplicate, and there's no fix PR. I confirmed on main 61cfb9b: vendor/jvm/gradle.rs writes no artifact-level maven-metadata.xml into .socket/vendor/gradle/<g>/<a>/, and the settings script's exclusiveContent { 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

  2. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Two more data points for this issue, on main 61cfb9b with Gradle 8.14.3 on Linux:

    • Version catalog range reproduces it. With libs.versions.toml set to text = { require = "[1.9,1.10.0]" } and implementation libs.text, a fresh clone resolves commons-text-1.10.0.jar before vendoring and commons-text-1.9.jar (unpatched, from the Gradle cache) after vendor exits 0. I saw it twice. The vendored 1.10.0 jar does carry the patch marker; it just isn't selected.
    • A gradle.lockfile masks it. With dependencyLocking { 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.jar after vendoring. So only unlocked range or catalog declarations are affected.

    Generated by Claude Code

  3. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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

  4. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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.0 is in a file repository, and its POM declares org.apache.commons:commons-text:[1.9,1.10.0]. The project only declares implementation 'com.example:lib:1.0' with repositories { maven { url = corp }; mavenCentral() }.
    • Before vendoring: commons-text:[1.9,1.10.0] -> 1.10.0.
    • vendor --json --offline for pkg:maven/org.apache.commons/commons-text@1.10.0 exits 0 with no warning. The planner can't see the range, because it's in a dependency's POM.
    • After vendoring: commons-text:[1.9,1.10.0] -> 1.9, from ~/.gradle/caches/modules-2/.../commons-text-1.9.jar (unpatched). vendor --check exits 0. vex --product … emits not_affected for 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

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