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

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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions