[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In a Gradle-only project (settings.gradle + build.gradle, repositories { mavenCentral() }), agent-mode apply resolves the Maven patch against the Maven local repository ($MAVEN_REPO_LOCAL / ~/.m2/repository) whenever that repository happens to hold the same GAV, for example from unrelated Maven use on the same machine or CI image. It patches that jar in place and exits 0 with applied: 1, and vex then emits not_affected / inline_mitigations_already_exist. Gradle never reads ~/.m2 unless the build declares mavenLocal(). It resolves from $GRADLE_USER_HOME/caches/modules-2/files-2.1/…, so the real build keeps compiling and running the unpatched jar.
This is the agent-mode analogue of #397 (NuGet) and #387 (cargo). It's separate from #349: #349 is the discovery gap (scan finds 0 packages and never fires). Here a manifest is already present (committed by a teammate, or saved by get), and apply + vex claim protection that the build doesn't have.
Impact
A false VEX attestation, plus a success exit code, for a vulnerability that's still present in the shipped Gradle build. As a side effect, the jar under ~/.m2 is mutated (with its .sha1 sidecar now stale) for every other Maven project on the machine.
Repro (Linux, Gradle 8.14.3, JDK 21, main 61cfb9b)
export GRADLE_USER_HOME=$PWD/gh MAVEN_REPO_LOCAL=$PWD/m2
mkdir proj && cd proj
echo "rootProject.name='p'" > settings.gradle
cat > build.gradle <<'EOF'
plugins { id 'java' }
repositories { mavenCentral() }
dependencies { implementation 'org.apache.commons:commons-text:1.10.0' }
tasks.register('printCp') { doLast { configurations.runtimeClasspath.files.each { println "CP " + it } } }
EOF
gradle -q printCp # fills $GRADLE_USER_HOME/caches/modules-2
mvn -q -Dmaven.repo.local=$MAVEN_REPO_LOCAL dependency:get -Dartifact=org.apache.commons:commons-text:1.10.0 # the same GAV, from unrelated Maven use
# stage an agent-mode record: files {"commons-text-1.10.0.jar": {beforeHash, afterHash}} + blob,
# where the patched jar adds a marker line to META-INF/NOTICE.txt
git init -q && git remote add origin https://github.com/example/gp.git
socket-patch apply --json --offline # "status":"success", applied: 1, exit 0
socket-patch vex --offline --product pkg:github/example/gp # status "not_affected"
unzip -p $MAVEN_REPO_LOCAL/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar META-INF/NOTICE.txt | grep -c MARKER # 1 (m2 copy patched)
gradle -q printCp | grep commons-text # …/gh/caches/modules-2/files-2.1/org.apache.commons/commons-text/1.10.0/3363381a…/commons-text-1.10.0.jar
unzip -p <that jar> META-INF/NOTICE.txt | grep -c MARKER # 0 (the build uses the unpatched jar)
I ran this twice from clean directories, and both runs gave the same result.
Control: with repositories { mavenLocal(); mavenCentral() } (and -Dmaven.repo.local pointing at the same m2), Gradle resolves m2/…/commons-text-1.10.0.jar and the marker is present. So the in-place patch itself is fine. The defect is that the CLI targets a copy that the build doesn't use, and attests it.
Expected vs actual
- Expected: docs/ecosystems.md (the Maven row) describes agent mode as in-place jar patching of
~/.m2, and the crawler deliberately accepts build.gradle* / settings.gradle* as project markers (maven_crawler.rs:581). For a Gradle build that has no mavenLocal(), the m2 copy isn't the installed package. apply should refuse or warn (for example, "Gradle builds resolve from $GRADLE_USER_HOME; use vendor"), or patch the copy Gradle actually uses. README.md says to "Generate VEX after installing to verify the copies your build consumes", so vex must not attest not_affected from a copy the build doesn't consume.
- Actual:
apply exits 0 with applied: 1, vex gives not_affected, and the Gradle build is unpatched. No warning is printed anywhere.
Matrix
| OS |
Gradle (JDK) |
Build repos |
apply |
vex |
Jar the build uses |
| Linux |
8.14.3 (21) |
mavenCentral() |
success, applied 1 ❌ |
not_affected ❌ |
unpatched (~/.gradle cache) ❌ |
| Linux |
8.14.3 (21) |
mavenLocal(); mavenCentral() |
success |
not_affected |
patched m2 jar ✅ (control) |
| macOS / Windows, Gradle 6–9 |
— |
— |
untested. The selection logic is OS- and version-independent, and the modules-2/files-2.1 layout has been unchanged since Gradle 1.x. |
|
|
Related
Suspect code
crates/socket-patch-core/src/crawlers/maven_crawler.rs:580-605: Gradle markers select m2_repo_path() (:709) as the install root, although a Gradle build without mavenLocal() never reads it.
crates/socket-patch-core/src/crawlers/maven_crawler.rs find_by_purls: Maven layout only, so the copy Gradle uses can never be patched or verified.
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In a Gradle-only project (
settings.gradle+build.gradle,repositories { mavenCentral() }), agent-modeapplyresolves the Maven patch against the Maven local repository ($MAVEN_REPO_LOCAL/~/.m2/repository) whenever that repository happens to hold the same GAV, for example from unrelated Maven use on the same machine or CI image. It patches that jar in place and exits 0 withapplied: 1, andvexthen emitsnot_affected/inline_mitigations_already_exist. Gradle never reads~/.m2unless the build declaresmavenLocal(). It resolves from$GRADLE_USER_HOME/caches/modules-2/files-2.1/…, so the real build keeps compiling and running the unpatched jar.This is the agent-mode analogue of #397 (NuGet) and #387 (cargo). It's separate from #349: #349 is the discovery gap (
scanfinds 0 packages and never fires). Here a manifest is already present (committed by a teammate, or saved byget), andapply+vexclaim protection that the build doesn't have.Impact
A false VEX attestation, plus a success exit code, for a vulnerability that's still present in the shipped Gradle build. As a side effect, the jar under
~/.m2is mutated (with its.sha1sidecar now stale) for every other Maven project on the machine.Repro (Linux, Gradle 8.14.3, JDK 21, main
61cfb9b)I ran this twice from clean directories, and both runs gave the same result.
Control: with
repositories { mavenLocal(); mavenCentral() }(and-Dmaven.repo.localpointing at the same m2), Gradle resolvesm2/…/commons-text-1.10.0.jarand the marker is present. So the in-place patch itself is fine. The defect is that the CLI targets a copy that the build doesn't use, and attests it.Expected vs actual
~/.m2, and the crawler deliberately acceptsbuild.gradle*/settings.gradle*as project markers (maven_crawler.rs:581). For a Gradle build that has nomavenLocal(), the m2 copy isn't the installed package.applyshould refuse or warn (for example, "Gradle builds resolve from$GRADLE_USER_HOME; usevendor"), or patch the copy Gradle actually uses. README.md says to "Generate VEX after installing to verify the copies your build consumes", sovexmust not attestnot_affectedfrom a copy the build doesn't consume.applyexits 0 withapplied: 1,vexgivesnot_affected, and the Gradle build is unpatched. No warning is printed anywhere.Matrix
mavenCentral()~/.gradlecache) ❌mavenLocal(); mavenCentral()modules-2/files-2.1layout has been unchanged since Gradle 1.x.Related
apply --global-prefix $GRADLE_USER_HOME/caches/modules-2/files-2.1fails loudly (package_not_installed, exit 1), becausefind_by_purlsonly knows the Maven layout. That's correct fail-loud behaviour, noted here for completeness.Suspect code
crates/socket-patch-core/src/crawlers/maven_crawler.rs:580-605: Gradle markers selectm2_repo_path()(:709) as the install root, although a Gradle build withoutmavenLocal()never reads it.crates/socket-patch-core/src/crawlers/maven_crawler.rsfind_by_purls: Maven layout only, so the copy Gradle uses can never be patched or verified.