[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In a Gradle multi-project build, running socket-patch vendor (or anything else that vendors, such as scan --mode vendored / get --mode vendored) from a subproject directory is treated as if that subproject were a standalone Gradle build:
- It reports
success / applied: 1 and exits 0.
- It creates
<sub>/settings.gradle with the apply from: '.socket/gradle/socket-patch.settings.gradle' line and <sub>/.socket/….
- The real build's root
settings.gradle is left untouched.
The result:
- Building from the root (
gradle :app:build, the normal invocation and what CI runs) still resolves the unpatched commons-text-1.10.0.jar from ~/.gradle/caches/modules-2, with exit 0.
- Building from inside the subproject (
cd app && gradle build) now fails. The new nested settings.gradle makes app its own root build, so it loses the root build's allprojects { repositories { … } }: Could not resolve all files for configuration ':runtimeClasspath'.
socket-patch vex, run in the subproject, attests not_affected / "Patched via Socket patch … (vendored)" for a build that doesn't use the patch.
Maven already refuses the equivalent situation: a discovered ancestor reactor gives vendor_jvm_shape_unsupported with reason: not_build_root: run vendor from reactor root …. Gradle has no such check.
Impact
This silently produces a false "patched" state, including a false VEX attestation, and breaks the subproject's standalone gradle invocation. Running a tool from the module you're working in is common in monorepos.
Repro
I used the e2e_vendor_jvm_build patch-service fixture (prebuilt_common::Server) and a staged .socket/manifest.json + blob for pkg:maven/org.apache.commons/commons-text@1.10.0, exactly as gradle_multi_project_vendor_locked_offline_tamper_and_byte_exact_revert does, with the manifest staged under app/.socket/.
proj/settings.gradle rootProject.name='s9'
include 'app'
proj/build.gradle allprojects { repositories { mavenCentral() } }
proj/app/build.gradle plugins { id 'java' }
dependencies { implementation 'org.apache.commons:commons-text:1.10.0' }
tasks.register('printCp') { doLast { configurations.runtimeClasspath.files.each { println 'CP ' + it } } }
cd proj/app && socket-patch vendor --json --offline --cwd . # exit 0, "applied": 1
git -C proj status # new: app/settings.gradle, app/.socket/** ; proj/settings.gradle unchanged
cd proj && gradle -q :app:printCp
# CP ~/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-text/1.10.0/<sha1>/commons-text-1.10.0.jar <- unpatched (no marker in META-INF/NOTICE.txt)
cd proj/app && gradle -q printCp
# FAILURE: Could not resolve all files for configuration ':runtimeClasspath'. > Could not resolve org.apache.commons:commons-text:1.10.0.
cd proj/app && socket-patch vex --offline --product pkg:maven/x/y@1
# "status": "not_affected", "impact_statement": "Patched via Socket patch 1d3c1fd2-… (vendored)"
Control: the same tree vendored from proj/ wires the root settings.gradle, and :app:printCp resolves .socket/vendor/gradle/…/commons-text-1.10.0.jar with the marker. That passes on Gradle 6.9.4, 7.6.6 and 9.8.0 (ubuntu probe) and on 8.14.3 locally.
Expected vs actual
- Expected:
docs/design/maven-vendoring.md says Gradle "multi-project builds" are supported. For Maven it says "Run vendoring from the reactor root. A discovered ancestor reactor produces not_build_root instead of allowing a partial submodule edit." Gradle multi-project builds should be guarded the same way. Either refuse with not_build_root when an ancestor settings.gradle(.kts) includes this directory, or wire the real root. A vendored success should mean the committed checkout actually builds against the patched artifact.
- Actual:
success, exit 0, a nested settings.gradle is created, the root build is unpatched, the subproject's standalone build is broken, and VEX attests not_affected.
OS × Gradle
| OS |
Gradle |
Reproduces |
| Linux |
8.14.3 (JDK 21) |
yes (2/2) |
| Linux |
9.8.0 (JDK 21) |
yes |
| macOS / Windows |
any |
not run. The logic is path-only (no OS-specific code), so I expect the same. |
First bad version: main 2463257 (#277). This is the first main with Gradle vendoring, and v4.0.0 refused gradle-only vendoring with vendor_gradle_unsupported.
Suspect code
crates/socket-patch-core/src/vendor/maven_repo.rs:285: the ancestor walk only checks maven_reactor::contains_module. It has no Gradle include / settings.gradle counterpart.
crates/socket-patch-core/src/vendor/jvm/mod.rs:229-236: detect treats any directory with a build.gradle(.kts) as a Gradle build root.
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In a Gradle multi-project build, running
socket-patch vendor(or anything else that vendors, such asscan --mode vendored/get --mode vendored) from a subproject directory is treated as if that subproject were a standalone Gradle build:success/applied: 1and exits 0.<sub>/settings.gradlewith theapply from: '.socket/gradle/socket-patch.settings.gradle'line and<sub>/.socket/….settings.gradleis left untouched.The result:
gradle :app:build, the normal invocation and what CI runs) still resolves the unpatchedcommons-text-1.10.0.jarfrom~/.gradle/caches/modules-2, with exit 0.cd app && gradle build) now fails. The new nestedsettings.gradlemakesappits own root build, so it loses the root build'sallprojects { repositories { … } }:Could not resolve all files for configuration ':runtimeClasspath'.socket-patch vex, run in the subproject, attestsnot_affected/ "Patched via Socket patch … (vendored)" for a build that doesn't use the patch.Maven already refuses the equivalent situation: a discovered ancestor reactor gives
vendor_jvm_shape_unsupportedwithreason: not_build_root: run vendor from reactor root …. Gradle has no such check.Impact
This silently produces a false "patched" state, including a false VEX attestation, and breaks the subproject's standalone
gradleinvocation. Running a tool from the module you're working in is common in monorepos.Repro
I used the
e2e_vendor_jvm_buildpatch-service fixture (prebuilt_common::Server) and a staged.socket/manifest.json+ blob forpkg:maven/org.apache.commons/commons-text@1.10.0, exactly asgradle_multi_project_vendor_locked_offline_tamper_and_byte_exact_revertdoes, with the manifest staged underapp/.socket/.Control: the same tree vendored from
proj/wires the rootsettings.gradle, and:app:printCpresolves.socket/vendor/gradle/…/commons-text-1.10.0.jarwith the marker. That passes on Gradle 6.9.4, 7.6.6 and 9.8.0 (ubuntu probe) and on 8.14.3 locally.Expected vs actual
docs/design/maven-vendoring.mdsays Gradle "multi-project builds" are supported. For Maven it says "Run vendoring from the reactor root. A discovered ancestor reactor producesnot_build_rootinstead of allowing a partial submodule edit." Gradle multi-project builds should be guarded the same way. Either refuse withnot_build_rootwhen an ancestorsettings.gradle(.kts)includes this directory, or wire the real root. A vendoredsuccessshould mean the committed checkout actually builds against the patched artifact.success, exit 0, a nestedsettings.gradleis created, the root build is unpatched, the subproject's standalone build is broken, and VEX attestsnot_affected.OS × Gradle
First bad version: main
2463257(#277). This is the first main with Gradle vendoring, and v4.0.0 refused gradle-only vendoring withvendor_gradle_unsupported.Suspect code
crates/socket-patch-core/src/vendor/maven_repo.rs:285: the ancestor walk only checksmaven_reactor::contains_module. It has no Gradleinclude/settings.gradlecounterpart.crates/socket-patch-core/src/vendor/jvm/mod.rs:229-236:detecttreats any directory with abuild.gradle(.kts)as a Gradle build root.