[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
The v5 Gradle vendoring planner refuses with gradle_exclusive_content_conflict when a user exclusiveContent rule claims the patched group. That check only runs on the settings files and the root build.gradle(.kts) (crates/socket-patch-core/src/vendor/jvm/gradle.rs:117-120, :156-158). It never sees the build scripts where repositories usually live in a multi-project build:
- a subproject's
app/build.gradle
- a
buildSrc (or build-logic) precompiled convention plugin such as buildSrc/src/main/groovy/bh.repos.gradle
In these shapes vendor exits 0 and reports applied. Every build of the vendored checkout then fails at resolution with Could not find org.apache.commons:commons-text:1.10.0, because the user's exclusive repository and socketPatchVendor both claim the module. vex attests not_affected for that build anyway.
This also breaks the documented hosted → vendored path. The hosted Gradle snippet (redirect_gradle_manual_snippet, docs/ecosystems.md "Gradle (hosted Maven)") is an exclusiveContent block that users paste into the build script that declares the dependency, which in a multi-project build is the subproject's script. If that snippet pins the base version (the legacy, non-suffixed form) and the user then runs vendor, they hit exactly this failure.
Impact
vendor reports success but leaves a build that can't resolve the patched dependency. It fails loudly, so the unpatched jar never ships. Still, CI breaks right after a "successful" vendor run, with an error that doesn't mention socket-patch.
vex emits not_affected / inline_mitigations_already_exist for a build that can't run.
- The same rule placed in root
allprojects {} / subprojects {} is refused correctly (control below), so this is an inconsistent refusal, not a design choice.
Repro
I used the repo's own vendored-JVM fixture: prebuilt_common::prepare_command + a staged .socket/manifest.json/blob for pkg:maven/org.apache.commons/commons-text@1.10.0, the same as e2e_vendor_jvm_build. Steps:
settings.gradle: rootProject.name = 'ex'
include 'app'
app/build.gradle: plugins { id 'java' }
repositories {
exclusiveContent {
forRepository { mavenCentral() }
filter { includeGroup 'org.apache.commons' }
}
mavenCentral()
}
dependencies { implementation 'org.apache.commons:commons-text:1.10.0' }
tasks.register('printCp') { def rc = configurations.runtimeClasspath
doLast { rc.files.each { println 'SOCKET-CP ' + it } } }
gradle :app:printCp: OK, resolves Central's jar.
- Stage the manifest + blob, then
socket-patch vendor --json: exit 0, "action": "applied", summary.failed = 0.
- Copy the committable files (no manifest or blobs) to a fresh checkout, then
gradle :app:printCp:
> Could not resolve all dependencies for configuration ':app:runtimeClasspath'.
> Could not find org.apache.commons:commons-text:1.10.0.
socket-patch vex --product pkg:maven/com.example/ex@1 --output vex.json: exit 0, "status": "not_affected" for commons-text.
Variants with the same result (vendor exit 0, then the build fails):
- conv: the same
repositories { exclusiveContent … } block in buildSrc/src/main/groovy/bh.repos.gradle (with id 'groovy-gradle-plugin'), applied by app with plugins { id 'bh.repos' }.
- snippet: the hosted snippet shape,
exclusiveContent { forRepository { maven { url "<hosted repo>" } } filter { includeVersion("org.apache.commons", "commons-text", "1.10.0") } }, in app/build.gradle.
Control (correct): the same block inside root build.gradle allprojects { … } or subprojects { … } gives vendor exit 1, vendor_jvm_shape_unsupported, reason: gradle_exclusive_content_conflict: build.gradle has an exclusiveContent rule for org.apache.commons; drop org.apache.commons:commons-text from it, and nothing is written.
Expected vs actual
- Expected: docs/design/maven-vendoring.md (Gradle) says "Android, Kotlin Multiplatform,
available-at module redirects, conflicting exclusive-content rules and paths leaving the checkout are refused." The check_exclusive_content doc comment says such a rule "would make ours unreachable ('Could not find')". Per CLI_CONTRACT a failed patch is reported as failed, so vendor should refuse (or at least warn) for every build script it can statically find. That includes included subproject scripts and buildSrc / literal includeBuild convention-plugin sources, the same way it already scans settings, buildSrc settings and included builds.
- Actual: only the root build script is checked. Subproject and convention-plugin rules are missed, so the result is a success report, a broken build, and a
not_affected VEX.
Matrix
| OS |
Gradle (JDK) |
sub |
conv (buildSrc) |
snippet in sub |
root allprojects/subprojects (control) |
| Linux |
8.14.3 (21) |
repro |
repro |
repro |
refused (correct) |
| Linux |
9.8.0 (21) |
repro |
repro |
repro |
not run |
| macOS / Windows |
— |
untested; the planner is a pure static scan, so I expect the same result |
|
|
|
Each cell ran twice across this run (8.14.3 for sub, conv and snippet; 9.8.0 for sub, conv and snippet). I didn't bisect: the Gradle vendoring backend is new in 2463257 (#277), which is the current main and is unreleased (latest release v4.0.0 refuses Gradle vendoring outright).
Suspect code
crates/socket-patch-core/src/vendor/jvm/gradle.rs:117-120: only read_build_script(read, "") (the root) goes through check_exclusive_content.
crates/socket-patch-core/src/vendor/jvm/gradle.rs:156-158: targets are settings files of the root, buildSrc and included builds. Subproject build scripts and convention-plugin sources aren't targets.
Tested on main 2463257.
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
The v5 Gradle vendoring planner refuses with
gradle_exclusive_content_conflictwhen a userexclusiveContentrule claims the patched group. That check only runs on the settings files and the rootbuild.gradle(.kts)(crates/socket-patch-core/src/vendor/jvm/gradle.rs:117-120,:156-158). It never sees the build scripts where repositories usually live in a multi-project build:app/build.gradlebuildSrc(orbuild-logic) precompiled convention plugin such asbuildSrc/src/main/groovy/bh.repos.gradleIn these shapes
vendorexits 0 and reportsapplied. Every build of the vendored checkout then fails at resolution withCould not find org.apache.commons:commons-text:1.10.0, because the user's exclusive repository andsocketPatchVendorboth claim the module.vexattestsnot_affectedfor that build anyway.This also breaks the documented hosted → vendored path. The hosted Gradle snippet (
redirect_gradle_manual_snippet, docs/ecosystems.md "Gradle (hosted Maven)") is anexclusiveContentblock that users paste into the build script that declares the dependency, which in a multi-project build is the subproject's script. If that snippet pins the base version (the legacy, non-suffixed form) and the user then runsvendor, they hit exactly this failure.Impact
vendorreports success but leaves a build that can't resolve the patched dependency. It fails loudly, so the unpatched jar never ships. Still, CI breaks right after a "successful" vendor run, with an error that doesn't mention socket-patch.vexemitsnot_affected/inline_mitigations_already_existfor a build that can't run.allprojects {}/subprojects {}is refused correctly (control below), so this is an inconsistent refusal, not a design choice.Repro
I used the repo's own vendored-JVM fixture:
prebuilt_common::prepare_command+ a staged.socket/manifest.json/blob forpkg:maven/org.apache.commons/commons-text@1.10.0, the same ase2e_vendor_jvm_build. Steps:gradle :app:printCp: OK, resolves Central's jar.socket-patch vendor --json: exit 0,"action": "applied",summary.failed = 0.gradle :app:printCp:socket-patch vex --product pkg:maven/com.example/ex@1 --output vex.json: exit 0,"status": "not_affected"for commons-text.Variants with the same result (vendor exit 0, then the build fails):
repositories { exclusiveContent … }block inbuildSrc/src/main/groovy/bh.repos.gradle(withid 'groovy-gradle-plugin'), applied byappwithplugins { id 'bh.repos' }.exclusiveContent { forRepository { maven { url "<hosted repo>" } } filter { includeVersion("org.apache.commons", "commons-text", "1.10.0") } }, inapp/build.gradle.Control (correct): the same block inside root
build.gradleallprojects { … }orsubprojects { … }givesvendorexit 1,vendor_jvm_shape_unsupported,reason: gradle_exclusive_content_conflict: build.gradle has an exclusiveContent rule for org.apache.commons; drop org.apache.commons:commons-text from it, and nothing is written.Expected vs actual
available-atmodule redirects, conflicting exclusive-content rules and paths leaving the checkout are refused." Thecheck_exclusive_contentdoc comment says such a rule "would make ours unreachable ('Could not find')". Per CLI_CONTRACT a failed patch is reported as failed, sovendorshould refuse (or at least warn) for every build script it can statically find. That includesincluded subproject scripts andbuildSrc/ literalincludeBuildconvention-plugin sources, the same way it already scans settings, buildSrc settings and included builds.not_affectedVEX.Matrix
Each cell ran twice across this run (8.14.3 for sub, conv and snippet; 9.8.0 for sub, conv and snippet). I didn't bisect: the Gradle vendoring backend is new in
2463257(#277), which is the current main and is unreleased (latest release v4.0.0 refuses Gradle vendoring outright).Suspect code
crates/socket-patch-core/src/vendor/jvm/gradle.rs:117-120: onlyread_build_script(read, "")(the root) goes throughcheck_exclusive_content.crates/socket-patch-core/src/vendor/jvm/gradle.rs:156-158: targets are settings files of the root, buildSrc and included builds. Subproject build scripts and convention-plugin sources aren't targets.Tested on main
2463257.