Skip to content

Vendored Gradle: running vendor from a subproject of a multi-project build exits 0 but wires a nested settings.gradle, so the real build stays unpatched and cd <subproject> && gradle breaks #428

Description

[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.

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